Skip to content

Instantly share code, notes, and snippets.

@PEZ
Created September 20, 2017 23:04
Show Gist options
  • Save PEZ/e4a790870855a0bb3a45da2da8f71aa3 to your computer and use it in GitHub Desktop.
Save PEZ/e4a790870855a0bb3a45da2da8f71aa3 to your computer and use it in GitHub Desktop.
Swift3 UIControl extension for adding block event listeners. Adapted from: https://stackoverflow.com/a/44917661/44639
import Foundation
import UIKit
extension UIControl {
func listen(_ action: @escaping () -> (), for controlEvents: UIControlEvents) -> AnyObject {
let sleeve = ClosureSleeve(attachTo: self, closure: action, controlEvents: controlEvents)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
return sleeve
}
func listenOnce(_ action: @escaping () -> (), for controlEvents: UIControlEvents) {
let sleeve = ClosureSleeve(attachTo: self, closure: action, controlEvents: controlEvents)
addTarget(sleeve, action: #selector(ClosureSleeve.invokeOnce), for: controlEvents)
}
func unlisten(sleeve: AnyObject) {
guard let sleeve = sleeve as? ClosureSleeve else { return }
self.removeTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: sleeve.controlEvents)
}
}
private class ClosureSleeve {
let closure: () -> ()
let controlEvents:UIControlEvents
let attachedTo: AnyObject
init(attachTo: AnyObject, closure: @escaping () -> (), controlEvents:UIControlEvents) {
self.attachedTo = attachTo
self.closure = closure
self.controlEvents = controlEvents
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}
@objc func invoke() {
closure()
}
@objc func invokeOnce() {
closure()
attachedTo.unlisten(sleeve: self)
}
}
// Register listener, keep the reference to unregister the listener
let listener = button.listenOnce({
print("I will say this every time you tap the button")
}, for: [.touchUpInside])
// … later …
button.unlisten(listener)
// Listen once for the control events, automatically unlisten when the block is performed
button.listenOnce({
print("I will only say this once")
}, for: [.touchUpInside, .touchDragExit])
@mariancerny
Copy link

Looks ok.

There is one line that looks dangerous to me:

attachedTo.unlisten(sleeve: self)

Because attachedTo is AnyObject, it will crash if called on other objects. Maybe wrapping that call with respondsToSelector would be an appropriate thing. Or Change the attachedTo variable to UIControl. However, that would not work if you want to use the ClosureSleeve for other stuff (I use it for example also for UIBarButtonItem and UITapGestureRecognizer). Maybe adding a protocol (like Unlistenable) could be a solution then.

I would also move all the closure parameters as the last parameter, so that trailing closure syntax can be used on call site.

Also, it looks like you cannot unlisten a listenOnce listener.

@PEZ
Copy link
Author

PEZ commented Sep 21, 2017

Wow, super duper feedback! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment