【问题标题】:How to correctly subclass UIControl?如何正确继承 UIControl?
【发布时间】:2010-11-17 17:32:53
【问题描述】:

我不想要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,因为这是我想要的来源:快速且无延迟的触摸坐标。

【问题讨论】:

  • 你绝对可以继承 UIControl,这些是正确的重写方法。您是通过编程方式还是通过 IB 实例化您的控件?如果是前者,您可以发布该代码吗?
  • 我也坚持这一点。任何人都有一个很好的教程来通过子类化 UIControl 来制作自定义控件?
  • 第三。我创建了一个 UIControl,如果我在控制器的 loadView 方法(无笔尖样式)中实例化它,它就可以正常工作。但是如果我从一个 nib 实例化一个,它的 touchesBegan:withEvent: 系列方法仍然会被调用,但 beginTrackingWithTouch:withEvent: 永远不会调用。莫名其妙。
  • 当这些方法没有被调用时,最可能的问题是你没有发起委托。

标签: ios objective-c uicontrol


【解决方案1】:

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

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

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

希望这会有所帮助。

【讨论】:

  • 这正是我的问题。我的自定义 UIControl 有一个吞下事件的子视图。将 userInteractionEnabled 设置为 NO 解决了!
  • 我知道这是旧的,但我想我会补充:你也可以覆盖- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; 默认情况下,它在子视图上调用-pointInside:withEvent: 并返回触摸应该去的那个。如果你重写它来调用自己的-pointInside:withEvent: 然后只返回 self 或 nil 子视图将不会收到任何触摸。
【解决方案2】:

斯威夫特

这些不仅仅是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 方法。如果您忘记添加冒号或没有正确数量的参数,您将遇到崩溃。
  • 感谢this answerTouchDragInside 事件的帮助。

传递其他数据

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

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

备注

  • 要了解有关委托模式的更多信息,请参阅this answer
  • 如果这些方法仅在自定义控件本身中使用,则无需使用委托。我本来可以添加一个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 实现委托模式。

【讨论】:

  • 我认为这将有资格成为公认的答案,因为它已经过深入研究并且与 Swift 相关的问题具有持续的相关性。 7 年来,原作者(如果他还活着,似乎不再是 SO)从未接受过另一个答案。也许这是给版主的。
  • @Greg,如果原始发帖人不再存在,实际上最好没有接受的答案,因为现在这个答案(如果值得的话)最终会上升到顶部。
【解决方案3】:

我一直在努力寻找解决这个问题的方法,但我认为没有。但是,在仔细检查文档后,我认为应该调用 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];
}

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

【讨论】:

  • 这是正确的。确保调用 [super touchesBegan:withEvent:] 等,以便在自定义 UIControl 正常工作时子类。
【解决方案4】:

我认为您忘记向 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");
}

【讨论】:

  • 我发现无论 a) 根本没有实现 touchesBegan 和 touchesMoved; b) 阻止他们并调用超级。无论哪种方式,都不会调用 beginTrackingWithTouch 和 continueTrackingWithTouch。
  • 这是一个有效的建议。如果我注释掉来自touchesBegan... 的[super] 呼叫,那么beginTracking... 将停止被呼叫。只要我再次调用 [super],它就会起作用。 +1 此外,这一切都无需像另一个答案中建议的那样手动调用beginTracking...
【解决方案5】:

我经常使用的最简单的方法是扩展 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 已经支持的相同控制事件(触摸、拖动等)

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

【讨论】:

  • 老兄,您自定义 UISwitch 的链接已失效... :(
【解决方案6】:

我遇到了 UIControl 没有响应 beginTrackingWithTouchcontinueTrackingWithTouch 的问题。

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

【讨论】:

    【解决方案7】:

    对象 C

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

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

    【讨论】:

      猜你喜欢
      • 2011-05-03
      • 2011-04-19
      • 1970-01-01
      • 1970-01-01
      • 2020-11-13
      • 1970-01-01
      • 1970-01-01
      • 2016-11-05
      • 2013-03-15
      相关资源
      最近更新 更多