如何正确地继承UIControl?


70

我不要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与指源自初始化实例化UIViewinitWithFrame:

我可以找到的所有示例都始终使用UIButtonUISlider作为子类的基础,但我想走得更近些,UIControl因为这就是我想要的源:快速且无延迟的Touch坐标。


您绝对可以继承UIControl的子类,而这些是要重写的正确方法。您是通过编程方式还是通过IB实例化控件?如果是前者,您可以发布该代码吗?
zpasternack

我也坚持这一点。任何人都有很好的教程,可以通过对UIControl进行子类化来制作自定义控件?
bentford,

3
第三。我创建了一个UIControl,如果我在控制器的loadView方法(无nib样式)中实例化它,它就可以正常工作。但是,如果我从笔尖实例化一个实例,它的touchesBegan:withEvent:方法族仍然会被调用,但是beginTrackingWithTouch:withEvent:永远不会。莫名其妙。
rgeorge


当不调用这些方法时,最可能的问题是您不初始化委托。
t1ser

Answers:


76

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

如果您有控制在所有子视图的任何,beginTrackingWithTouchtouchesBegan,等会不会被调用,因为这些子视图吞咽触摸事件。

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

希望这可以帮助。


7
这正是我的问题。我的自定义UIControl有一个吞下事件的子视图。设置userInteractionEnabled为NO解决了!
nrj 2012年

3
我知道这很老,但我想补充一下:- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;默认情况下,您也可以覆盖它调用-pointInside:withEvent:子视图并返回触摸应到达的视图。如果您覆盖它以调用-pointInside:withEvent:自己,然后仅返回self或nil,则子视图将不会受到任何触摸。
凯尔·豪威尔斯2015年

29

迅速

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

在此处输入图片说明

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


方法1:添加目标

一个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方法。在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那样实现委托模式。

1
我认为这将是可以接受的答案,因为它已经过深入研究,并且与Swift相关的问题持续相关。在最初的7年中,原作者(如果他还活着,似乎不再是SO的人)从未接受过另一个答案。也许这是给主持人的。
格雷格

2
@Greg,如果原来的海报不再存在,那么最好不要接受任何答案,因为现在这个答案(如果有的话)最终可以上升到顶部。
Suragch

20

为了解决这个问题,我已经花了很长时间寻找解决方案,但我认为没有解决方案。然而,在文档的仔细检查,我认为它可能是一个误会begintrackingWithTouch:withEvent:,并continueTrackingWithTouch:withEvent:应该在所有被称为...

UIControl 文档说:

您可能要UIControl 出于以下两个基本原因扩展子类:

观察或修改针对特定事件向目标发送动作消息的方法。为此,请重写 sendAction:to:forEvent:,评估传入的选择器,目标对象或“注释”位掩码,然后按要求进行。

要提供自定义跟踪行为(例如,更改高亮外观)要做到这一点,覆盖一个或全部下列方法: beginTrackingWithTouch:withEvent:continueTrackingWithTouch:withEvent:endTrackingWithTouch:withEvent:

我认为这不是很清楚,其中的关键部分是您可能要扩展UIControl子类,而不是UIControl直接扩展。这是可能的beginTrackingWithTouch:withEvent:continuetrackingWithTouch:withEvent:不应该被调用响应触摸和UIControl直接子类都应该给他们打电话让他们的子类可以监视跟踪。

因此,我的解决方案是重写touchesBegan:withEvent:touchesMoved:withEvent:从此处调用它们,如下所示。请注意,此控件未启用多点触摸,并且我不在乎触摸结束和触摸取消的事件,但是如果您想完整/彻底,则也应该实现这些功能。

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

请注意,您还应该使用发送UIControlEvent*与控件相关的任何消息sendActionsForControlEvents:-可以从super方法中调用这些消息,但我尚未对其进行测试。


这是对的。确保调用[super touchesBegan:withEvent:]等,以便子类在自定义UIControl正常工作的情况下使用。
samuraisam

10

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

因此,只需简单地继承UIControl的子类,然后在init方法中(如果使用nibs,请确保还设置了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:选择器注册,但是您可以从上面的代码中看出这一点)


1
老兄,您到自定义UISwitch的链接已死... :(
nickfox 2011年

9

我认为您忘了将[super]调用添加到touchesBegan / touchesEnded / touchesMoved。像

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

3
我发现不管a)根本没有实现touchesBegan和touchesMoved;b)强迫他们并称其为超级。无论哪种方式,beginTrackingWithTouch和continueTrackingWithTouch都不会被调用。
jhabbott

这是一个有效的建议。如果我注释掉[super]调用,touchesBegan...beginTracking...不再被调用。只要我再次调用[super],它就会起作用。+1此外,这一切都不必beginTracking...像另一个答案中所建议的那样手动调用。
塞巴斯蒂安·马丁

4

我在UIControl无法响应beginTrackingWithTouch和时遇到麻烦continueTrackingWithTouch

我发现我的问题是,当我initWithFrame:CGRectMake()将框架做得很小(反应区域)时,只有一点点可以工作。我将框架设置为与控件相同的大小,然后每次在控件中的任何位置按下它都会响应。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.