Create a gist now

Instantly share code, notes, and snippets.

Embed
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

This comment has been minimized.

Show comment
Hide comment
@mariancerny

mariancerny Sep 21, 2017

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.

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

This comment has been minimized.

Show comment
Hide comment
@PEZ

PEZ Sep 21, 2017

Wow, super duper feedback! Thanks!

Owner

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