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

I currently have 4 UITextField

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

      

and I want to track their various events:

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

      

but I don't want to have 3 event handlers separately, so I created one such function. This function does a great job of reporting which UITextField fired the event, but it doesn't tell me which event was fired .:

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

      

How can I print both the sender name and the name of the event being triggered (ex: .EditingDidEnd, .EditingDidEnd, .EditingDidEnd)?

Ideally, I don't want to write multiple event handlers, I would rather have one function.

Something like that:

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

      

+3


source to share


3 answers


Unfortunately, you cannot tell which control event triggered the action handler. This has nothing to do with Swift; it's just a feature of Cocoa.

This is an odd design decision, but it is. See, for example, my book that complains about this:



Curiously, none of the action selector parameters provide a way to know which control event caused the current action selector to be called! Thus, for example, to distinguish a Touch Up Inside control event from a Touch Up Outside control event, their respective target action pairs must specify two different action handlers; if you send them to a single handler, that handler will not be able to detect which control event occurred.

+5


source


As Matt said, this is not possible (and it is really very annoying!). I used this little helper class to save myself some typing.

Each UIControl.Event

has a corresponding optional Selector

. You can only set the selectors you want and ignore the ones you don't need.

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

      



Use it like this:

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

      

Though, to be honest, in the end it really doesn't save you typing so much.

0


source


Alternatively, you can use this little helper that converts UIEvent

(or UITouch

) to UIControl.Event

. It works by checking for a Phase

touch, locating its location in the send view, and comparing it to its previous location. If you use UIEvent

it will use the first touch.

However, be warned: it doesn't .touchDownRepeat

handle well .touchDownRepeat

. An tapCount

object property UIEvent

has a longer time duration than normally triggered .touchDownRepeat

. It also seems like sending multiple actions to .touchDownRepeat

.

And of course it doesn't handle others UIControl.Event

like .editingDidBegin

etc.

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

}

      

Use it like this:

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

      

To implement .touchDownRepeat

you can wrap this method in a small class and save time on every touch, or just save the click time in your control.

0


source







All Articles