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
}
}
source to share
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.
source to share
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.
source to share
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.
source to share