5
\$\begingroup\$

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

    func on<T: UIControl>(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<T: UIControl>(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(self, ptr, handler, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
        return ptr
    }

    func off(identifier: EventHandlerId) {
        if let handler = objc_getAssociatedObject(self, identifier) as? EventHandler {
            self.removeTarget(handler, action: nil, forControlEvents: .AllEvents)
            objc_setAssociatedObject(self, identifier, nil, 0)
        }
    }
}

Usage:

class MyViewController:UIViewController {

    @IBOutlet weak var myButton:UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // add event handler
        let handle = myButton.on(.TouchUpInside) { (sender, event) in
            // `sender` is the control itself.
            // `event` is the UIEvent object.
        }

        // remove event handler
        myButton.off(handle)

        // event handler can be () -> Void type
        myButton.on(.TouchUpInside) { [unowned self] in
            // your work
        }

        // "oneshot" event handler
        myButton.once(.TouchUpInside) {
            // your work
        }
    }
}

It's working OK... But, for example:

let btn = UIButton()
btn.on(.TouchUpInside) { (sender: UISwitch, _) in /* ... */ }
                                  ^^^^^^^^

This compiles, but I want to make this compile time error.

Currently, as a workaround, I'm doing "assertion":

assert(self.isKindOfClass(T), "The handler must receive \(NSStringFromClass(self.dynamicType)) or UIControl")

And, if possible, I wish the compiler could infer the type of the sender, like this:

let btn = UIButton()
btn.on(.TouchUpInside) { sender, _ in
    sender.titleLabel.text = "Pushed !!"
}

Any ideas?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ For what it's worth, I've posted a question on Stack Overflow, which is relevant to your question here. stackoverflow.com/questions/27434346/… \$\endgroup\$
    – nhgrif
    Commented Dec 11, 2014 at 23:28
  • \$\begingroup\$ What you may and may not do after receiving answers \$\endgroup\$
    – rolfl
    Commented Dec 12, 2014 at 11:39
  • \$\begingroup\$ @rolfl I apologize for my fault. But, I think, I did not edit the original code. So, where should I write such reply, then? \$\endgroup\$
    – rintaro
    Commented Dec 12, 2014 at 12:04
  • \$\begingroup\$ Did you read option 1 on the meta link I added? Accept/vote for answers that help. Only post changed code if you want that revised code reviewed as well, in which case it should be in a different question. \$\endgroup\$
    – rolfl
    Commented Dec 12, 2014 at 12:08

1 Answer 1

3
\$\begingroup\$

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?

enter image description here

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

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

\$\endgroup\$
3
  • \$\begingroup\$ Thanks. Please see my updated OP. Anyway, I see I need more consideration about "naming". BTW, I think, UnsafePointer<EventHandler> is meaningless. EventHandler is already a reference type. So UnsafePointer<EventHandler> means "pointer to pointer to object". \$\endgroup\$
    – rintaro
    Commented Dec 12, 2014 at 7:40
  • \$\begingroup\$ This is extraordinarily common throughout Apple's libraries. ABAddressBookRef, etc. \$\endgroup\$
    – nhgrif
    Commented Dec 12, 2014 at 11:27
  • \$\begingroup\$ Actually, I see what you are saying... But in that case then we don't need the typealias at all since we are making the EventHandler class public, we don't need to have pointers to it. \$\endgroup\$
    – nhgrif
    Commented Dec 12, 2014 at 12:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.