在 Swift 中,您如何检测哪些 UIControl 事件触发了操作?

In Swift, how do you detect which UIControlEvents triggered the action?

我目前有 4 个 UITextField

@IBOutlet weak var fNameTextField: UITextField!
@IBOutlet weak var lNameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var phoneTextField: UITextField!

我想跟踪他们的各种活动:

[UIControlEvents.EditingChanged, UIControlEvents.EditingDidBegin, UIControlEvents.EditingDidEnd ]

但我不想有 3 个独立的事件处理程序,所以我创建了一个这样的函数。这个函数很好地告诉我哪个 UITextField 触发了一个事件,但它没有告诉我触发了哪个事件。:

fNameTextField.addTarget(self, action: "onChangeTextField:", forControlEvents: UIControlEvents.AllTouchEvents)
lNameTextField.addTarget(self, action: "onChangeTextField:", forControlEvents: UIControlEvents.AllTouchEvents)
emailTextField.addTarget(self, action: "onChangeTextField:", forControlEvents: UIControlEvents.AllTouchEvents)
phoneTextField.addTarget(self, action: "onChangeTextField:", forControlEvents: UIControlEvents.AllTouchEvents)

func onChangeTextField(sender:UITextField){
    switch(sender){
        case fNameTextField:
            print("First Name")
        case lNameTextField:
            print("Last Name")
        case emailTextField:
            print("E-mail")
        case phoneTextField:
            print("Phone")
        default: break
    }
}

如何打印发件人姓名和触发事件的名称(例如:.EditingDidEnd、.EditingDidEnd、.EditingDidEnd)?

理想情况下,我不想编写多个事件处理程序,我更喜欢一个函数。

像这样:

func onChangeTextField(sender:UITextField){
    switch(sender.eventTriggerd){
        case UIControlEvents.EditingChanged:
            println("EditingChanged")
        case UIControlEvents.EditingDidBegin:
            println("EditingDidBegin")
        case UIControlEvents.EditingDidEnd:
            println("EditingDidEnd")
        default: break
    }
}

遗憾的是,您无法区分哪个控制事件触发了动作处理程序。这与Swift无关;这只是 Cocoa.

的一个功能

这是一个奇怪的设计决定,但事实就是如此。参见,例如,my book,它抱怨它:

Curiously, none of the action selector parameters provide any way to learn which control event triggered the current action selector call! Thus, for example, to distinguish a Touch Up Inside control event from a Touch Up Outside control event, their corresponding target–action pairs must specify two different action handlers; if you dispatch them to the same action handler, that handler cannot discover which control event occurred.

正如马特所说,这是不可能的(而且确实很烦人!)。我一直在使用这个小帮手 class 来减轻自己的一些打字负担。

每个UIControl.Event都有对应的可选Selector。您可以只设置需要的选择器,忽略不需要的选择器。

class TargetActionMaker<T: UIControl> {

    var touchDown: Selector?
    var touchDownRepeat: Selector?
    var touchDragInside: Selector?
    var touchDragOutside: Selector?
    var touchDragEnter: Selector?
    var touchDragExit: Selector?
    var touchUpInside: Selector?
    var touchUpOutside: Selector?
    var touchCancel: Selector?
    var valueChanged: Selector?
    var primaryActionTriggered: Selector?
    var editingDidBegin: Selector?
    var editingChanged: Selector?
    var editingDidEnd: Selector?
    var editingDidEndOnExit: Selector?
    var allTouchEvents: Selector?
    var allEditingEvents: Selector?
    var applicationReserved: Selector?
    var systemReserved: Selector?
    var allEvents: Selector?

    func addActions(_ sender: T, target: Any?) {
        for selectorAndEvent in self.selectorsAndEvents() {
            if let action = selectorAndEvent.0 {
                sender.addTarget(target, action: action, for: selectorAndEvent.1)
            }
        }
    }

    private func selectorsAndEvents() -> [(Selector?, UIControl.Event)] {
        return [
            (self.touchDown, .touchDown),
            (self.touchDownRepeat, .touchDownRepeat),
            (self.touchDragInside, .touchDragInside),
            (self.touchDragOutside, .touchDragOutside),
            (self.touchDragEnter, .touchDragEnter),
            (self.touchDragExit, .touchDragExit),
            (self.touchUpInside, .touchUpInside),
            (self.touchUpOutside, .touchUpOutside),
            (self.touchCancel, .touchCancel),
            (self.valueChanged, .valueChanged),
            (self.primaryActionTriggered, .primaryActionTriggered),
            (self.editingDidBegin, .editingDidBegin),
            (self.editingChanged, .editingChanged),
            (self.editingDidEnd, .editingDidEnd),
            (self.editingDidEndOnExit, .editingDidEndOnExit),
            (self.allTouchEvents, .allTouchEvents),
            (self.allEditingEvents, .allEditingEvents),
            (self.applicationReserved, .applicationReserved),
            (self.systemReserved, .systemReserved),
            (self.allEvents, .allEvents)
        ]
    }
}

像这样使用它:

class MyControl: UIControl {

    func setupSelectors() {
        let targetActionMaker = TargetActionMaker<MyControl>()
        targetActionMaker.touchDown = #selector(self.handleTouchDown(_:))
        targetActionMaker.touchUpInside = #selector(self.handleTouchUpInside(_:))
        targetActionMaker.touchUpOutside = #selector(self.handleTouchUpOutside(_:))
        targetActionMaker.addActions(self, target: self)
    }

    @objc func handleTouchDown(_ sender: MyControl) {
        print("handleTouchDown")
    }

    @objc func handleTouchUpInside(_ sender: MyControl) {
        print("handleTouchUpInside")
    }

    @objc func handleTouchUpOutside(_ sender: MyControl) {
        print("handleTouchUpOutside")
    }
}

不过,老实说,最终它并没有让你省去那么多打字。

或者,您可以使用这个小助手将 UIEvent(或 UITouch)转换为 UIControl.Event。它的工作原理是检查触摸的 Phase,获取它在发送视图中的位置,并将它与之前的位置进行比较。如果您使用 UIEvent 它将使用第一次触摸。

但是请注意:它不能很好地处理 .touchDownRepeatUIEventtapCount 属性 比正常触发的 .touchDownRepeat 的定时持续时间更长。此外,它似乎在 .touchDownRepeat 上发送多个操作。

而且,当然,它不会处理其他 UIControl.Event,例如 .editingDidBegin

public extension UIEvent {

    func firstTouchToControlEvent() -> UIControl.Event? {
            guard let touch = self.allTouches?.first else {
                print("firstTouchToControlEvent() Error: couldn't get the first touch. \(self)")
            return nil
        }
        return touch.toControlEvent()
    }

}

public extension UITouch {

    func toControlEvent() -> UIControl.Event? {
        guard let view = self.view else {
            print("UITouch.toControlEvent() Error: couldn't get the containing view. \(self)")
            return nil
        }
        let isInside = view.bounds.contains(self.location(in: view))
        let wasInside = view.bounds.contains(self.previousLocation(in: view))
        switch self.phase {
        case .began:
            if isInside {
                if self.tapCount > 1 {
                    return .touchDownRepeat
                }
                return .touchDown
            }
            print("UITouch.toControlEvent() Error: unexpected touch began outs1ide of view. \(self)")
            return nil
        case .moved:
            if isInside && wasInside {
                return .touchDragInside
            } else if isInside && !wasInside {
                return .touchDragEnter
            } else if !isInside && wasInside {
                return .touchDragExit
            } else if !isInside && !wasInside {
                return .touchDragOutside
            } else {
                print("UITouch.toControlEvent() Error: couldn't determine touch moved boundary. \(self)")
                return nil
            }
        case .ended:
            if isInside {
                return .touchUpInside
            } else {
                return.touchUpOutside
            }
        case .cancelled:
            return .touchCancel
        default:
            print("UITouch.toControlEvent() Warning: couldn't handle touch event. \(self)")
            return nil
        }
    }

}

像这样使用它:

class TestControl: UIControl {

    func setupTouchEvent() {
        self.addTarget(self, action: #selector(handleTouchEvent(_:forEvent:)), for: .allTouchEvents)
    }

    @objc func handleTouchEvent(_ sender: TestControl, forEvent event: UIEvent) {
        guard let controlEvent = event.firstTouchToControlEvent() else {
            print("Error: couldn't convert event to control event: \(event)")
            return
        }
        switch controlEvent {
        case .touchDown:
            print("touchDown")
        case .touchDownRepeat:
            print("touchDownRepeat")
        case .touchUpInside:
            print("touchUpInside")
        case .touchUpOutside:
            print("touchUpOutside")
        case .touchDragEnter:
            print("touchDragEnter")
        case .touchDragExit:
            print("touchDragExit")
        case .touchDragInside:
            print("touchDragInside")
        case .touchDragOutside:
            print("touchDragOutside")
        default:
            print("Error: couldn't convert event to control event, or unhandled event case: \(event)")
        }
    }
}

要实现 .touchDownRepeat,您可以将此方法包装在一点点 class 中,并节省每次触地的时间,或者只需将点击时间保存在您的控件中。