如何正确地子类化UIControl?

77

我不想使用任何像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;
}

即使使用UIView的指定初始化方法initWithFrame:实例化 UIControl,它们永远不会被调用。

我能找到的所有示例都是以UIButtonUISlider为基础进行子类化,但我想更接近UIControl,因为它是我想要的源:快速和无延迟的触摸坐标。


你肯定可以子类化UIControl,并且那些是正确的要重写的方法。你是通过程序编写实例化控件,还是通过IB?如果是前者,你可以发布该代码吗? - zpasternack
我也遇到了同样的问题。有人有关于通过子类化UIControl来创建自定义控件的好教程吗? - bentford
3
同意。如果我在控制器的loadView方法中实例化一个UIControl,它可以很好地工作(无nib样式)。但是,如果我从nib实例化一个UIControl,它的touchesBegan:withEvent:系列方法仍然会被调用,但beginTrackingWithTouch:withEvent:永远不会被调用。令人困惑。 - rgeorge
Swift答案 - Suragch
当这些方法没有被调用时,最有可能的问题是您没有初始化委托。 - t1ser
7个回答

87

我知道这个问题很古老了,但我遇到了同样的问题,所以我想发表一下我的看法。

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

如果您不希望这些子视图处理触摸事件,可以将userInteractionEnabled设置为NO,这样子视图就会简单地通过事件。然后,您可以重写touchesBegan/touchesEnded并在那里管理所有的触摸操作。

希望这可以帮助你。


9
这正是我的问题所在。我的自定义 UIControl 有一个子视图,它会屏蔽掉事件。将 userInteractionEnabled 设置为 NO 就解决了这个问题! - nrj
4
我知道这个问题有些老了,但我想补充一下:你也可以重写 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; 方法。默认情况下,它会在子视图上调用 -pointInside:withEvent: 并返回触摸应该到达的那个子视图。如果你重写它,在自己身上调用 -pointInside:withEvent: 然后只返回 self 或 nil,那么子视图就不会收到任何触摸事件。 - Kyle Howells

32

Swift

有多种方法可以对UIControl进行子类化。当父视图需要响应触摸事件或从控件获取其他数据时,通常使用(1)目标或(2)通过覆盖触摸事件的委托模式来完成。为了完整起见,我还将展示如何使用手势识别器(3)完成相同的事情。每种方法都会像以下动画一样运作:

enter image description here

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


方法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(没有冒号)。 冒号表示正在将参数传递给操作方法。 在操作方法中,指定该参数是对您的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)
}

方法二:委托模式和重写触摸事件

通过子类化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"
    }
}

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

方法三:使用手势识别器

在任何视图上都可以添加手势识别器,它也适用于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上)从未接受过其他答案。也许这是为了管理员考虑。 - Greg
2
@Greg,如果原帖作者已经不在了,实际上没有被接受的答案更好,因为现在这个答案(如果它值得)最终可以升至顶部。 - Suragch

20
我已经长时间寻找解决这个问题的方法,但我认为并没有解决方法。不过,在仔细检查文档后,我认为可能是一个误解,即 begintrackingWithTouch:withEvent:continueTrackingWithTouch:withEvent: 不应该被调用... UIControl 文档中说:
您可能想要扩展 UIControl 的子类有两个基本原因:
- 观察或修改对特定事件的目标发送动作消息的分派 - 为此,请重写 sendAction:to:forEvent:,评估传入的选择器、目标对象或“Note”位掩码,并根据需要继续进行。
- 提供自定义跟踪行为(例如更改高亮外观) - 为此,请重写以下一个或多个方法: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];
}

请注意你还应使用 sendActionsForControlEvents: 发送与控件相关的任何UIControlEvent*消息 - 这些消息可能会从父类方法中调用,我没有测试过。


这是正确的。确保调用 [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

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

3
我发现无论是 a) 完全不实现touchesBegan和touchesMoved方法;还是 b) 实现并调用super方法, 无论哪种方式,beginTrackingWithTouch和continueTrackingWithTouch都不会被调用。 - jhabbott
这是一个有效的建议。如果我从touchesBegan...中注释掉[super]调用,那么beginTracking...就不会被调用了。只要我再次调用[super],它就可以正常工作。此外,这一切都不需要像另一个答案中建议的那样手动调用beginTracking... - Sebastien Martin

4

我遇到了一个UIControl不响应beginTrackingWithTouchcontinueTrackingWithTouch的问题。

我发现我的问题是当我使用initWithFrame:CGRectMake()时,我把框架设得太小了(反应区域),只有几个点可以工作。我将框架设为与控件相同的大小,然后每次按下控件上的任何位置都会有响应。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接