mikeash.com: just this guy, you know?

Posted at 2015-12-25 15:11 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A 2016-01-29: Swift Struct Storage
Previous article: Friday Q&A 2015-12-11: Swift Weak References
Tags: cocoa fridayqna swift
Friday Q&A 2015-12-25: Swifty Target/Action
by Mike Ash  

Cocoa's target/action system for responding to controls is a great system for Objective-C, but is a bit unnatural to use in Swift. Today, I'm going to explore building a wrapper that allows using a Swift function as the action.

Overview
The target/action system is great for things like menu items which might command many different objects depending on context. For example, the Paste menu item connects to whatever object in the responder chain happens to implement a paste: method at the time.

It's less great when you're setting the target and and action in code and there's only ever one target object, as is commonly the case for buttons, text fields, and other such controls. It ends up being an exercise in stringly typed code and tends to be a bit error-prone. It also forces the action to be implemented separately, even when it's simple and would naturally fit inline.

Implementing a pure Swift control that accepted a function as its action would be simple, but for real code we still need to deal with Cocoa. The goal is to adapt NSControl to allow setting a function as the target, while still coexisting with the target/action system.

NSControl doesn't have any good hooks to intercept the sending of the action, so instead we'll just co-opt the existing targe/action mechanism. This requires an adapter object to act as the target. When it receives the action, it will then invoke the function passed to it.

First Attempt
Let's get started with some code. Here is an adapter object that holds a function and calls that function when its action method is called:

    class ActionTrampoline: NSObject {
        var action: NSControl -> Void

        init(action: NSControl -> Void) {
            self.action = action
        }

        @objc func action(sender: NSControl) {
            action(sender)
        }
    }

Here's an addition to NSControl that wraps the creation of the trampoline and setting it as the target:

    extension NSControl {
        @nonobjc func setAction(action: NSControl -> Void) {
            let trampoline = ActionTrampoline(action: action)
            self.target = trampoline
            self.action = "action:"
        }
    }

(The @nonobjc annotation allows this to coexist with the Objective-C action property on NSControl. Without it, this method would need a different name.)

Let's try it out:

    let button = NSButton()
    button.setAction({ print("Action from \($0)") })
    button.sendAction(button.action, to: button.target)

Oops, nothing happens.

Extending the Trampoline's Lifetime
This first attempt doesn't work, because target is a weak property. There are no strong references to trampoline to keep it alive, so it's deallocated immediately. Then the call to sendAction sends the action to nil, which does nothing.

We need to extend the trampoline's lifetime. We could return it to the caller and require them to keep it around somewhere, but that would be inconvenient. A better way is to tie the trampoline's lifetime to that of the control. We can accomplish this using associated objects.

We start by defining a key for use with the associated objects API. This is a little less convenient than in Objective-C, because Swift is not so friendly about taking the address of variables, and in fact doesn't guarantee that the pointer produced by using & on variable will be consistent. Instead of trying to use the address of a global variable, this code just allocates a bit of memory and uses that address:

    let NSControlActionFunctionAssociatedObjectKey = UnsafeMutablePointer<Int8>.alloc(1)

The NSControl extension then uses objc_setAssociatedObject to attach the trampoline to the control. Although the value is never retrieved, simply setting it ensures that it is kept alive as long as the control is alive:

    extension NSControl {
        @nonobjc func setAction(action: NSControl -> Void) {
            let trampoline = ActionTrampoline(action: action)
            self.target = trampoline
            self.action = "action:"

            objc_setAssociatedObject(self, NSControlActionFunctionAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
        }
    }

Let's try it again:

    let button = NSButton()
    button.setAction({ print("Action from \($0)") })
    button.sendAction(button.action, to: button.target)

This time it works!

    Action from <NSButton: 0x7fe1019124d0>

Making it Generic
This first version works fine, but the types aren't quite right. The parameter to the function is always NSControl. That means that while the above test code works, this does not:

    button.setAction({ print("Action from \($0.title)") })

This will fail to compile, because NSControl doesn't have a title property. We know that the parameter is actually an NSButton, but the compiler doesn't know that. In Objective-C, we simply declare the proper type in the method and the compiler has to trust us. In Swift, we have to educate the compiler about the types.

We could make setAction generic, something like:

    func setAction<T>(action: T -> Void) { ...

But this requires an explicit type on the function passed in, so $0 would no longer work. You'd have to write something like:

    button.setAction({ (sender: NSButton) in ...

It would be a lot better to have type inference work for us.

Swift's Self type exists for this purpose. Self denotes the actual type of self, like instancetype does for Objective-C. For example:

    extension NSControl {
        func frobnitz() -> Self {
            Swift.print("Frobbing \(self)")
            return self
        }
    }

    button.frobnitz().title = "hello"

Let's use this in the NSControl extension to make it generic:

    extension NSControl {
        @nonobjc func setAction(action: Self -> Void) {

Oops, we can't:

    error: 'Self' is only available in a protocol or as the result of a method in a class; did you mean 'NSControl'?

Fortunately, the error message suggests a way forward: put the method in a protocol. Start with an empty protocol:

    protocol NSControlActionFunctionProtocol {}

Let's change the name of the associated object key to fit its new home, while we're at it:

    let NSControlActionFunctionProtocolAssociatedObjectKey = UnsafeMutablePointer<Int8>.alloc(1)

We'll need a generic version of ActionTrampoline. This it much like the original version, but the implementation of action requires a forced cast since @objc methods aren't allowed to refer to a generic type:

    class ActionTrampoline<T>: NSObject {
        var action: T -> Void

        init(action: T -> Void) {
            self.action = action
        }

        @objc func action(sender: NSControl) {
            action(sender as! T)
        }
    }

The method implementation is basically the same as before, just with Self instead of NSControl. Constraining the extension to Self: NSControl lets us use all NSControl methods and properties on self, like target and action:

    extension NSControlActionFunctionProtocol where Self: NSControl {
        func setAction(action: Self -> Void) {
            let trampoline = ActionTrampoline(action: action)
            self.target = trampoline
            self.action = "action:"
            objc_setAssociatedObject(self, NSControlActionFunctionProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
        }
    }

Finally, we need to make NSControl conform to this protocol in an extension. Since the protocol itself is empty, the extension can be empty too:

    extension NSControl: NSControlActionFunctionProtocol {}

Let's try it out!

    let button = NSButton()
    button.setAction({ (button: NSButton) in
        print("Action from \(button.title)")
    })
    button.sendAction(button.action, to: button.target)

This prints:

    Action from Button

Success!

UIKit Version
Adopting this code for use in UIKit is easy. UIControl can have multiple targets for a variety of different events, so we just need to allow passing in the events as a parameter, and use addTarget to add the trampoline:

    class ActionTrampoline<T>: NSObject {
        var action: T -> Void

        init(action: T -> Void) {
            self.action = action
        }

        @objc func action(sender: UIControl) {
            print(sender)
            action(sender as! T)
        }
    }

    let NSControlActionFunctionProtocolAssociatedObjectKey = UnsafeMutablePointer<Int8>.alloc(1)

    protocol NSControlActionFunctionProtocol {}
    extension NSControlActionFunctionProtocol where Self: UIControl {
        func addAction(events: UIControlEvents, _ action: Self -> Void) {
            let trampoline = ActionTrampoline(action: action)
            self.addTarget(trampoline, action: "action:", forControlEvents: events)
            objc_setAssociatedObject(self, NSControlActionFunctionProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    extension UIControl: NSControlActionFunctionProtocol {}

Testing it:

    let button = UIButton()
    button.addAction([.TouchUpInside], { (button: UIButton) in
        print("Action from \(button.titleLabel?.text)")
    })
    button.sendActionsForControlEvents([.TouchUpInside])

    Action from nil

Apparently UIButton doesn't set a title by default like NSButton does. But, success!

A Note on Retain Cycles
It's easy to make retain cycles using this call. For example:

    button.addAction({ _ in
        self.doSomething()
    })

If you hold a strong reference to button (note that even if you declare button as weak, you'll indirectly hold a strong reference to it if you hold a strong reference to a view that contains it, or its window) then this will create a cycle that will cause your object to leak.

As is usually the case with cycles, the answer is to capture self either as weak or unowned:

    button.addAction({ [weak self] _ in
        self?.doSomething()
    })

Optional chaining keeps the body easy to read. Or if you're sure that the action will never, ever be invoked after self is destroyed, use [unowned self] instead to get a weak reference which doesn't need to be nil checked since it will fail loudly if self is ever destroyed early.

Conclusion
Making a Swift-y adapter for Cocoa target/actions is fairly straightforward. Memory management means we have to work a little to keep the trampoline object alive, but associated objects solve that problem. Making a method that has a parameter that's generic on the type of self requires jumping through some hoops, but a protocol extension makes it possible.

That's it for today. I'll be back with more goodies in the new year. Friday Q&A is driven by reader ideas, so if you have any topics you'd like to see covered in 2016 or beyond, please send them in!

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle. Click here for more information.

Comments:

Hello Mike! Great article as always.

I think you have a bug in the UIKit version. You are always setting the associated object for the same key. If you try to assign an action for another event, target for the previous one will be deallocated:

let button = UIButton()
button.setTitle("my button", forState: .Normal)
button.addAction([.TouchUpInside], {
    print("TouchUpInside action from \($0.titleLabel?.text)")
})
button.addAction([.TouchDown], {
    print("TouchDown action from \($0.titleLabel?.text)")
})
button.sendActionsForControlEvents([.TouchUpInside])
button.sendActionsForControlEvents([.TouchDown])

Output:
TouchDown action from Optional("my button")
There is a clear practical reason why original target-action pattern implementation creates weak relationship. But when converted to block-based API this weak semantics is no longer enforced by the API and becomes responsibility of the programmer which is bad. And implicit capture semantics of blocks/closures makes it hard to maintain and error-prone in practice – as per your note on retain cycles.
 
Given that we can solve 'stringly-typed' problem but still force weak relationship semantics:

class ActionTrampoline<T: AnyObject, C>: NSObject {
    weak var target: T?
    var method: T -> C -> Void
    
    let associatedObjectKey = UnsafeMutablePointer<Int8>.alloc(1)
    
    init(target: T, method: T -> C -> Void) {
        self.target = target
        self.method = method
    }
    
    @objc func action(sender: UIControl) {
        if let target = target {
            method(target)(sender as! C)
        }
    }
}

protocol NSControlActionFunctionProtocol {}
extension NSControlActionFunctionProtocol where Self: UIControl {
    func addTarget<T: AnyObject>(target: T, method: T -> Self -> Void, events: UIControlEvents) {
        let trampoline = ActionTrampoline(target: target, method: method)
        self.addTarget(trampoline, action: "action:", forControlEvents: events)
        objc_setAssociatedObject(self, trampoline.associatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
    }
}
extension UIControl: NSControlActionFunctionProtocol {}

class Controller {
    let button = UIButton()
    
    init() {
        button.addTarget(self, method: self.dynamicType.handleButton, events: [.TouchUpInside])
    }
    
    func handleButton(button: UIButton) {
        print("Action from \(button.titleLabel?.text)")
    }
}

let controller = Controller()
controller.button.sendActionsForControlEvents([.TouchUpInside])
Max O: but now you're just back to the existing Cocoa target-action API...
There was an early discussion on swift-evolution about accepting closures wherever selectors appear. Joe Groff had an idea to address retain cycle concerns by using something like @convention(objc_selector) to enforce a context-free closure:

https://lists.swift.org/pipermail/swift-evolution/2015-December/000200.html
I believe this to be very complicated. Instead, I store a weak-referencing "MultiClosure", and have each control be its own target.

Example usage:
textField.editingDidEndOnExit📡 += onEditingDidEndOnExit

Where onEditingDidEndOnExit is an "EquatableClosure".

Hit me up @jessyMeow on Twitter if interested.
One would also have to make sure to not reference the control from within the action closure, but use the passed argument instead. It is tempting to just use

button.setAction { _ in
    print("Action from: \(button.title)")
}

… rather than

button.setAction { button in
    print("Action from: \(button.title)")
}

And since the control holds a strong reference to the trampoline (through the associated object mechanism) and the trampoline may hold a strong reference to the control, by referencing the variable from the surrounding scope in the closure, this easily leads to retain cycles as well.
AppKit version updated for Swift 3...


let NSControlActionFunctionProtocolAssociatedObjectKey = UnsafeMutablePointer<Int8>.allocate(capacity:1)

class ActionTrampoline<T>: NSObject {
    var action: (T) -> Void
    init(action: @escaping (T) -> Void) {
        self.action = action
    }
    @objc func action(_ sender: NSControl) {
        action(sender as! T)
    }
}

protocol NSControlActionFunctionProtocol {}

extension NSControlActionFunctionProtocol where Self: NSControl {
    func setAction(action: @escaping (Self) -> Void) {
        let trampoline = ActionTrampoline(action: action)
        self.target = trampoline
        self.action = Selector("action:")
        objc_setAssociatedObject(self, NSControlActionFunctionProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
    }
}

extension NSControl: NSControlActionFunctionProtocol {}
<\code>

Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
The Answer to the Ultimate Question of Life, the Universe, and Everything?
Comment:
Formatting: <i> <b> <blockquote> <code>.
NOTE: Due to an increase in spam, URLs are forbidden! Please provide search terms or fragment your URLs so they don't look like URLs.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.