HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Closure as UIControlEvents handler

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
uicontroleventshandlerclosure

Problem

I'm expanding the functionality of this SO answer.

Mainly I'm focusing on:

  • You can add as many handlers as you want.



  • You can remove (i.e. cancel) the handler.



  • You can receive the sender parameter in the handler closure.



  • You can add "oneshot" handler.



```
import UIKit

extension UIControl {
private class EventHandler {

let _cb: (UIControl, UIEvent) -> Void
let _oneshot: Bool

init(_ cb: (UIControl, UIEvent) -> Void, oneshot: Bool) {
_cb = cb
_oneshot = oneshot
}

@objc func invoke(sender: UIControl, event: UIEvent) {
_cb(sender, event)
if _oneshot {
sender.off(unsafeAddressOf(self))
}
}
}

typealias EventHandlerId = UnsafePointer

func on(events: UIControlEvents, _ callback: (T, UIEvent) -> Void) -> EventHandlerId {
assert(self.isKindOfClass(T), "The handler must receive \(NSStringFromClass(self.dynamicType)) or UIControl")
return self._on(events, EventHandler({ callback($0 as T, $1) }, oneshot: false))
}
func once(events: UIControlEvents, _ callback: (T, UIEvent) -> Void) -> EventHandlerId {
assert(self.isKindOfClass(T), "The handler must receive \(NSStringFromClass(self.dynamicType)) or UIControl")
return self._on(events, EventHandler({ callback($0 as T, $1) }, oneshot: true))
}

func on(events: UIControlEvents, _ callback: () -> Void) -> EventHandlerId {
return self._on(events, EventHandler({ _, _ in callback() }, oneshot: false))
}
func once(events: UIControlEvents, _ callback: () -> Void) -> EventHandlerId {
return self._on(events, EventHandler({ _, _ in callback() }, oneshot: true))
}

private func _on(events: UIControlEvents, _ handler: EventHandler) -> EventHandlerId {
let ptr = unsafeAddressOf(handler)
self.addTarget(handler, action: "invoke:event:", forControlEvents: events)
objc_setAssociatedObject(s

Solution

Consider for a moment, if you will, how we typically handle a user tapping a button. We typically will do so with an @IBAction method. What are the Apple suggested defaults when we hook up a button this way?

The default is to give us a method that takes an object of type AnyObject as its only parameter. We can, of course, change the type to specify UIButton (or whatever UIControl subclass our element actually is). We can also change the sent arguments. We can choose between None, Sender, and Sender and Event.

I don't think it's useful to allow objects that aren't of type UIControl as the parameter. But, I do feel like we're missing methods to accept a handler that just passes the Sender argument. For me, this is the most common way I hook up methods (mostly because it's the default, but sometimes because I want the sender). Meanwhile, I don't think I've ever cared about the Event argument.

But moreover, I actually think it'd also be useful if we also had the option of passing a reference to the handler itself (the EventHandlerId returned when the handler is added) so that potentially we could then remove the event conditionally and without having to keep track of the handler reference on our own. (Although admittedly, I haven't played too terribly much with this aspect of your code, so I'm not sure how possible or manageable that might be.)

I'd change your typealias to this:

typealias EventHandlerRef = UnsafePointer


and remove the private access modifier from your EventHandler class, and add version of all your methods that take an EventHandler argument.

For starters, I'd do this because even though the version you've implemented is easier, there's not a particularly good reason for this aspect of the implementation to be private.

But moreover, by making the EventHandler class private, you've tricked yourself into writing the off function in a not-so-great way. The off function takes an EventHandler pointer, finds the event, then removes it as a target of the UI element. And that's fine for everything you've done... except for your "oneshot" implementation... where the EventHandler object finds its own reference to pass to the control's off method so that method can use the pointer to find the object (which we already knew) and remove it.

Instead, we should have a method that accepts an EventHandler object. The method that accepts the EventHandlerRef grabs the EventHandler that the reference points to and calls the method that accepts the EventHandler.

The reason, by the way, I prefer EventHandlerRef to EventHandlerId is two-fold. First of all, the value it holds is hardly an id in the sense that one might think in a database sense, but most importantly, suffixes pointer typedefs with Ref is just how Apple does it throughout their own libraries. Any other iOS developer will see EventHandlerRef and know that this is a pointer to an EventHandler thing, even if they don't know what sort of thing EventHandler is. Meanwhile, EventHandlerId seems slightly arbitrary.

Other than these two observations, my main complaint here would be naming. Large, descriptive variable names don't make your compiled program take up any more space on the disk, or any more RAM, or run any slower. The only drawback to large, descriptive variable names is that it makes your code more readable. Wait... that's not a drawback.

Moreover, this isn't Objective-C. Something declares as private truly is private in Swift. The underscores just make things look weird here. So, here's my take on your code, including the name changes I'd make:

Well, I was going to post some sample code... but now I'm going to be distracted with writing a bug report because the compiler keeps crashing trying to figure out the sample I wrote. And that means there's something wrong with it, but the compiler doesn't feel like telling me, so I can't figure it out...

Code Snippets

typealias EventHandlerRef = UnsafePointer<EventHandler>

Context

StackExchange Code Review Q#73364, answer score: 3

Revisions (0)

No revisions yet.