73

我不想要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;
}

他们永远不会被调用,即使UIControl是用指定初始化程序从UIView,实例化的initWithFrame:

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

4

7 回答 7

83

我知道这个问题很古老,但我遇到了同样的问题,我想我应该给我 2 美分。

如果您的控件根本没有任何子视图beginTrackingWithTouch,则touchesBegan, 等可能不会被调用,因为这些子视图正在吞噬触摸事件。

如果您不希望这些子视图处理触摸,您可以设置userInteractionEnabledNO,因此子视图只需传递事件即可。然后,您可以在那里覆盖touchesBegan/touchesEnded和管理您的所有触摸。

希望这可以帮助。

于 2011-09-25T01:53:57.383 回答
32

迅速

这些是子类化的不止一种方式UIControl。当父视图需要对触摸事件做出反应或从控件获取其他数据时,通常使用 (1) 目标或 (2) 具有覆盖触摸事件的委托模式来完成。为了完整起见,我还将展示如何 (3) 使用手势识别器做同样的事情。这些方法中的每一个都将类似于以下动画:

在此处输入图像描述

您只需要选择以下方法之一。


方法一:添加目标

UIControl类支持已内置的目标。如果您不需要将大量数据传递给父类,这可能是您想要的方法。

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)"
        }
    }
}

笔记

  • 动作方法名称没有什么特别之处。我可以给他们打电话。我只需要小心地拼写方法名称,就像我在添加目标时所做的那样。否则你会崩溃。
  • 中的两个冒号didDragInsideControl:withEvent:表示将两个参数传递给该didDragInsideControl方法。如果您忘记添加冒号或没有正确数量的参数,您将遇到崩溃。
  • 感谢此答案TouchDragInside活动的帮助。

传递其他数据

如果您的自定义控件有一些价值

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

您想在目标操作方法中访问,然后您可以传入对控件的引用。设置目标时,在操作方法名称后添加一个冒号。例如:

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

请注意,它是touchedDown:(带冒号)而不是touchedDown(不带冒号)。冒号表示正在将参数传递给操作方法。在 action 方法中,指定参数是对您的UIControl子类的引用。通过该引用,您可以从控件中获取数据。

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

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

方法 2:委托模式和覆盖触摸事件

子类UIControl化使我们可以访问以下方法:

  • beginTrackingWithTouch当手指第一次在控件的范围内向下触摸时调用。
  • continueTrackingWithTouch当手指滑过控件甚至超出控件的边界时会重复调用。
  • endTrackingWithTouch当手指离开屏幕时调用。

如果您需要对触摸事件进行特殊控制,或者如果您需要与父级进行大量数据通信,那么这种方法可能比添加目标更有效。

这是如何做到的:

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

这就是将视图控制器设置为委托并响应我们自定义控件的触摸事件的方式。

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"
    }
}

笔记

  • 要了解有关委托模式的更多信息,请参阅此答案
  • 如果仅在自定义控件本身中使用这些方法,则无需使用委托和这些方法。我本可以添加一个print语句来显示事件是如何被调用的。在这种情况下,代码将简化为

    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")
        }
    }
    

方法 3:使用手势识别器

添加手势识别器可以在任何视图上完成,它也适用于UIControl. 为了获得与顶部示例相似的结果,我们将使用UIPanGestureRecognizer. 然后通过测试触发事件时的各种状态,我们可以确定发生了什么。

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"
        }
    }
}

笔记

  • 不要忘记在action: "gestureRecognized:". 冒号表示正在传入参数。
  • 如果您需要从控件中获取数据,您可以按照上面的方法 2 实现委托模式。
于 2016-01-13T10:35:01.827 回答
20

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.

于 2010-10-13T12:14:54.097 回答
10

我经常使用的最简单的方法是扩展 UIControl,但使用继承的 addTarget 方法来接收各种事件的回调。关键是同时监听发送者和事件,以便您可以找到有关实际事件的更多信息(例如事件发生的位置)。

因此,只需简单地将 UIControl 子类化,然后在 init 方法中(如果您使用 nib,请确保您的 initWithCoder 也已设置),添加以下内容:

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

当然,您可以选择任何标准控件事件,包括 UIControlEventAllTouchEvents。请注意,选择器将传递两个对象。首先是控制。第二个是有关事件的信息。这是一个使用触摸事件来切换按钮的示例,具体取决于用户是否按下了左侧和右侧。

- (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
        }
    }
}

当然,这是用于非常简单的控件,您可能会达到这样的程度,即这不会为您提供足够的功能,但是到目前为止,这对于自定义控件的所有目的都有效,并且非常易于使用,因为我通常关心相同UIControl 已经支持的控制事件(修饰、拖动等)

此处自定义控件的代码示例: 自定义 UISwitch (注意:这不会向 buttonPressed:forEvent: 选择器注册,但您可以从上面的代码中弄清楚)

于 2009-11-17T18:44:34.370 回答
10

我认为您忘记向 touchesBegan/touchesEnded/touchesMoved 添加 [super] 调用。方法如

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

如果您像这样覆盖 touchesBegan / touchesEnded 则不起作用:

- (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");
}

但!如果方法如下所示,一切正常:

- (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");
}
于 2009-11-18T15:03:35.010 回答
4

我遇到了 UIControl 不响应beginTrackingWithTouchand的问题continueTrackingWithTouch

我发现我的问题是当我initWithFrame:CGRectMake()把框架做得很小(反应区域)并且只有几个点可以工作。我使框架与控件的大小相同,然后随时按下控件中的任何位置它都会响应。

于 2013-05-16T14:24:34.290 回答
0

对象 C

我从 2014 年找到了一篇相关文章https://www.objc.io/issues/13-architecture/behaviors/

有趣的是,它的方法是利用 IB 并将事件处理逻辑封装在指定对象中(他们称之为行为),从而从视图/视图控制器中删除逻辑,使其更轻。

于 2016-05-07T11:05:12.543 回答