如何正确子类化 UIControl?

我不想要 UIButton或者类似的东西。我想直接子类化 UIControl并制作我自己的,非常特殊的控件。

但由于某种原因,我重写的所有方法都没有被调用。目标操作工具起作用,目标接收到适当的操作消息。然而,在我的 UIControl子类中,我必须捕获触摸坐标,而这样做的唯一方法似乎是覆盖这些家伙:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"begin touch track");
return YES;
}


- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"continue touch track");
return YES;
}

即使使用来自 UIViewinitWithFrame:的指定初始化程序实例化了 UIControl,也不会调用它们。

我能找到的所有例子总是使用 UIButtonUISlider作为子类化的基础,但是我想更接近于 UIControl,因为它是我想要的: 快速和无延迟触摸坐标的来源。

46103 次浏览

The simplest approach that I often use is to extend UIControl, but to make use of the inherited addTarget method to receive callbacks for the various events. The key is to listen for both the sender and the event so that you can find out more information about the actual event (such as the location of where the event occurred).

So, just simply subclass UIControl and then in the init method (make sure your initWithCoder is also setup if you are using nibs) , add the following:

[self addTarget:self action:@selector(buttonPressed:forEvent:) forControlEvents:UIControlEventTouchUpInside];

Of course, you can choose any of the standard control events including UIControlEventAllTouchEvents. Notice that the selector will get passed two objects. The first is the control. The second is the info about the event. Here's an example of using the touch event to toggle a button depending on if the user pressed on the left and right.

- (IBAction)buttonPressed:(id)sender forEvent:(UIEvent *)event
{
if (sender == self.someControl)
{
UITouch* touch = [[event allTouches] anyObject];
CGPoint p = [touch locationInView:self.someControl];
if (p.x < self. someControl.frame.size.width / 2.0)
{
// left side touch
}
else
{
// right side touch
}
}
}

Granted, this is for pretty simplistic controls and you may reach a point where this will not give you enough functionality, but this has worked for all my purposes for custom controls to this point and is really easy to use since I typically care about the same control events that the UIControl already supports (touch up, drag, etc...)

Code sample of custom control here: Custom UISwitch (note: this does not register with the buttonPressed:forEvent: selector, but you can figure that out from the code above)

I think that you forgot to add [super] calls to touchesBegan/touchesEnded/touchesMoved. Methods like

(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event

aren't working if you overriding touchesBegan / touchesEnded like this :

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Touches Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Touches Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"Touches Ended");
}

But! All works fine if methods will be like :

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
NSLog(@"Touches Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
NSLog(@"Touches Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
NSLog(@"Touches Ended");
}

I have looked long and hard for a solution to this problem and I don't think there is one. However, on closer inspection of the documentation I think it might be a misunderstanding that begintrackingWithTouch:withEvent: and continueTrackingWithTouch:withEvent: are supposed to be called at all...

UIControl documentation says:

You may want to extend a UIControl subclass for two basic reasons:

To observe or modify the dispatch of action messages to targets for particular events To do this, override sendAction:to:forEvent:, evaluate the passed-in selector, target object, or “Note” bit mask and proceed as required.

To provide custom tracking behavior (for example, to change the highlight appearance) To do this, override one or all of the following methods: beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent:, endTrackingWithTouch:withEvent:.

The critical part of this, which is not very clear in my view, is that it says you may want to extend a UIControl subclass - NOT you may want to extend UIControl directly. It's possible that beginTrackingWithTouch:withEvent: and continuetrackingWithTouch:withEvent: are not supposed to get called in response to touches and that UIControl direct subclasses are supposed to call them so that their subclasses can monitor tracking.

So my solution to this is to override touchesBegan:withEvent: and touchesMoved:withEvent: and call them from there as follows. Note that multi-touch is not enabled for this control and that I don't care about touches ended and touches canceled events, but if you want to be complete/thorough you should probably implement those too.

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
[super touchesBegan:touches withEvent:event];
// Get the only touch (multipleTouchEnabled is NO)
UITouch* touch = [touches anyObject];
// Track the touch
[self beginTrackingWithTouch:touch withEvent:event];
}


- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
[super touchesMoved:touches withEvent:event];
// Get the only touch (multipleTouchEnabled is NO)
UITouch* touch = [touches anyObject];
// Track the touch
[self continueTrackingWithTouch:touch withEvent:event];
}

Note that you should also send any UIControlEvent* messages that are relevant for your control using sendActionsForControlEvents: - these may be called from the super methods, I haven't tested it.

I know this question is ancient, but I had the same problem and I thought I should give my 2 cents.

If your control has any subviews at all, beginTrackingWithTouch, touchesBegan, etc might not get called because those subviews are swallowing the touch events.

If you don't want those subviews to handle touches, you can set userInteractionEnabled to NO, so the subviews simply passes the event through. Then you can override touchesBegan/touchesEnded and manage all your touches there.

Hope this helps.

I was having trouble with a UIControl not responding to beginTrackingWithTouch and continueTrackingWithTouch.

I found my problem was when I did initWithFrame:CGRectMake() I made the frame to small (the area that reacts) and had only a couple point spot where it did work. I made the frame the same size as the control and then anytime I pressed on anywhere in the control it responded.

Swift

These is more than one way to subclass UIControl. When a parent view needs to react to touch events or get other data from the control, this is usually done using (1) targets or (2) the delegate pattern with overridden touch events. For completeness I will also show how to (3) do the same thing with a gesture recognizer. Each of these methods will behave like the following animation:

enter image description here

You only need to choose one of the following methods.


Method 1: Add a Target

A UIControl subclass has support for targets already built in. If you don't need to pass a lot of data to the parent, this is probably the method you want.

MyCustomControl.swift

import UIKit
class MyCustomControl: UIControl {
// You don't need to do anything special in the control for targets to work.
}

ViewController.swift

import UIKit
class ViewController: UIViewController {


@IBOutlet weak var myCustomControl: MyCustomControl!
@IBOutlet weak var trackingBeganLabel: UILabel!
@IBOutlet weak var trackingEndedLabel: UILabel!
@IBOutlet weak var xLabel: UILabel!
@IBOutlet weak var yLabel: UILabel!


override func viewDidLoad() {
super.viewDidLoad()


// Add the targets
// Whenever the given even occurs, the action method will be called
myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)
myCustomControl.addTarget(self, action: #selector(didDragInsideControl(_:withEvent:)),
forControlEvents: UIControlEvents.TouchDragInside)
myCustomControl.addTarget(self, action: #selector(touchedUpInside), forControlEvents: UIControlEvents.TouchUpInside)
}


// MARK: - target action methods


func touchedDown() {
trackingBeganLabel.text = "Tracking began"
}


func touchedUpInside() {
trackingEndedLabel.text = "Tracking ended"
}


func didDragInsideControl(control: MyCustomControl, withEvent event: UIEvent) {


if let touch = event.touchesForView(control)?.first {
let location = touch.locationInView(control)
xLabel.text = "x: \(location.x)"
yLabel.text = "y: \(location.y)"
}
}
}

Notes

  • There is nothing special about the action method names. I could have called them anything. I just have to be careful to spell the method name exactly like I did where I added the target. Otherwise you get a crash.
  • The two colons in didDragInsideControl:withEvent: means that two parameters are being passed to the didDragInsideControl method. If you forget to add a colon or if you don't have the correct number of parameters, you will get a crash.
  • Thanks to this answer for help with the TouchDragInside event.

Passing other data

If you have some value in your custom control

class MyCustomControl: UIControl {
var someValue = "hello"
}

that you want to access in the target action method, then you can pass in a reference to the control. When you are setting the target, add a colon after the action method name. For example:

myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)

Notice that it is touchedDown: (with a colon) and not touchedDown (without a colon). The colon means that a parameter is being passed to the action method. In the action method, specify that the parameter is a reference to your UIControl subclass. With that reference, you can get data from your control.

func touchedDown(control: MyCustomControl) {
trackingBeganLabel.text = "Tracking began"


// now you have access to the public properties and methods of your control
print(control.someValue)
}

Method 2: Delegate Pattern and Override Touch Events

Subclassing UIControl gives us access to the following methods:

  • beginTrackingWithTouch is called when the finger first touches down within the control's bounds.
  • continueTrackingWithTouch is called repeatedly as the finger slides across the control and even outside of the control's bounds.
  • endTrackingWithTouch is called when the finger lifts off the screen.

If you need special control of the touch events or if you have a lot of data communication to do with the parent, then this method may work better then adding targets.

Here is how to do it:

MyCustomControl.swift

import UIKit


// These are out self-defined rules for how we will communicate with other classes
protocol ViewControllerCommunicationDelegate: class {
func myTrackingBegan()
func myTrackingContinuing(location: CGPoint)
func myTrackingEnded()
}


class MyCustomControl: UIControl {


// whichever class wants to be notified of the touch events must set the delegate to itself
weak var delegate: ViewControllerCommunicationDelegate?


override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {


// notify the delegate (i.e. the view controller)
delegate?.myTrackingBegan()


// returning true means that future events (like continueTrackingWithTouch and endTrackingWithTouch) will continue to be fired
return true
}


override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {


// get the touch location in our custom control's own coordinate system
let point = touch.locationInView(self)


// Update the delegate (i.e. the view controller) with the new coordinate point
delegate?.myTrackingContinuing(point)


// returning true means that future events will continue to be fired
return true
}


override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {


// notify the delegate (i.e. the view controller)
delegate?.myTrackingEnded()
}
}

ViewController.swift

This is how the view controller is set up to be the delegate and respond to touch events from our custom control.

import UIKit
class ViewController: UIViewController, ViewControllerCommunicationDelegate {


@IBOutlet weak var myCustomControl: MyCustomControl!
@IBOutlet weak var trackingBeganLabel: UILabel!
@IBOutlet weak var trackingEndedLabel: UILabel!
@IBOutlet weak var xLabel: UILabel!
@IBOutlet weak var yLabel: UILabel!


override func viewDidLoad() {
super.viewDidLoad()
myCustomControl.delegate = self
}


func myTrackingBegan() {
trackingBeganLabel.text = "Tracking began"
}


func myTrackingContinuing(location: CGPoint) {
xLabel.text = "x: \(location.x)"
yLabel.text = "y: \(location.y)"
}


func myTrackingEnded() {
trackingEndedLabel.text = "Tracking ended"
}
}

Notes

  • To learn more about the delegate pattern, see this answer.
  • It is not necessary to use a delegate with these methods if they are only being used within the custom control itself. I could have just added a print statement to show how the events are being called. In that case, the code would be simplified to

    import UIKit
    class MyCustomControl: UIControl {
    
    
    override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
    print("Began tracking")
    return true
    }
    
    
    override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
    let point = touch.locationInView(self)
    print("x: \(point.x), y: \(point.y)")
    return true
    }
    
    
    override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
    print("Ended tracking")
    }
    }
    

Method 3: Use a Gesture Recognizer

Adding a gesture recognizer can be done on any view and it also works on a UIControl. To get similar results to the example at the top, we will use a UIPanGestureRecognizer. Then by testing the various states when an event is fired we can determine what is happening.

MyCustomControl.swift

import UIKit
class MyCustomControl: UIControl {
// nothing special is required in the control to make it work
}

ViewController.swift

import UIKit
class ViewController: UIViewController {


@IBOutlet weak var myCustomControl: MyCustomControl!
@IBOutlet weak var trackingBeganLabel: UILabel!
@IBOutlet weak var trackingEndedLabel: UILabel!
@IBOutlet weak var xLabel: UILabel!
@IBOutlet weak var yLabel: UILabel!


override func viewDidLoad() {
super.viewDidLoad()


// add gesture recognizer
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognized(_:)))
myCustomControl.addGestureRecognizer(gestureRecognizer)
}


// gesture recognizer action method
func gestureRecognized(gesture: UIPanGestureRecognizer) {


if gesture.state == UIGestureRecognizerState.Began {


trackingBeganLabel.text = "Tracking began"


} else if gesture.state == UIGestureRecognizerState.Changed {


let location = gesture.locationInView(myCustomControl)


xLabel.text = "x: \(location.x)"
yLabel.text = "y: \(location.y)"


} else if gesture.state == UIGestureRecognizerState.Ended {
trackingEndedLabel.text = "Tracking ended"
}
}
}

Notes

  • Don't forget to add the colon after the action method name in action: "gestureRecognized:". The colon means that parameters are being passed in.
  • If you need to get data from the control, you could implement the delegate pattern as in method 2 above.

Obj C

I found a related article from 2014 https://www.objc.io/issues/13-architecture/behaviors/.

The interesting thing about it is that its approach is to make use of IB and encapsulate the event handling logic inside a designated object (they call it behaviors), thus removing logic from your view/viewController making it lighter.