Adding Closures to Buttons in Swift

One of the things I still find a little backward and unnatural to use in Swift is the Target-Action pattern.

Adding Closures to Buttons in Swift

One of the things I still find a little backward and unnatural to use in Swift is the Target-Action pattern.

You can avoid this for the most part if you're using interface builder and you have all of your actions hooked up with IBOutlets.

Apple did eventually give us #selector. This added type safety and code completion, but that hardly feels like a real solution. You still have to use the @objc flag to expose the called methods to Objective-C, and the syntax is a little ugly. It certainly doesn’t feel Swifty…

Closures are Swifty. So let’s add some of those!

We’ll start by setting up our wrapper class and begin filling out our extension with some boilerplate. This will help us get around the stored property restrictions we currently have with extensions in Swift.

UIBarButtonItem

extension UIBarButtonItem {
    
    /// Typealias for UIBarButtonItem closure.
    private typealias UIBarButtonItemTargetClosure = (UIBarButtonItem) -> ()

    private class UIBarButtonItemClosureWrapper: NSObject {
        let closure: UIBarButtonItemTargetClosure
        init(_ closure: @escaping UIBarButtonItemTargetClosure) {
            self.closure = closure
        }
    }
    
    private struct AssociatedKeys {
        static var targetClosure = "targetClosure"
    }
    
    private var targetClosure: UIBarButtonItemTargetClosure? {
        get {
            guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? UIBarButtonItemClosureWrapper else { return nil }
            return closureWrapper.closure
        }
        set(newValue) {
            guard let newValue = newValue else { return }
            objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, UIBarButtonItemClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

UIBarButtonItem is a special case as it has an initialiser that accepts a target and action as parameters, whereas UIButton allows you to modify its target-action after initialisation. This is because UIBarButtonItem is a direct subclass of UIBarButton, which is a subclass of NSObject. Neither of which have target-action methods. Whereas UIButton inherits from UIControl, which is where it gets its target-action from.

So for UIBarButtonItem, what we're going to do is create a new convenience initialiser which will do some extra setup before calling the designated initialiser.

Add the following code to the bottom of the extension:

convenience init(title: String?, style: UIBarButtonItem.Style, closure: @escaping UIBarButtonItemTargetClosure) {
    self.init(title: title, style: style, target: nil, action: nil)
    targetClosure = closure
    action = #selector(UIBarButtonItem.closureAction)
}

@objc func closureAction() {
    guard let targetClosure = targetClosure else { return }
    targetClosure(self)
}

We start off by calling self.init, apply our title and style, but leave target-action empty. Afterwards we store a reference to the passed in closure into our targetClosure that we created earlier.

Then finally the action property on UIBarButtonItem is set to a selector, which will run the closure stored in targetClosure any time the action is triggered, such as tapping the button.

Under the hood the same operations are happening, so all we've really done is add a little extra layer of abstraction. One huge benefit here is that you now have access to a central area to run some other reusable code such as logging button presses, or capturing analytics.

This is how you can now set up your UIBarButtonItem.

// Old way of doing things.
let actionButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(someMethod()))

// New way of doing things.
let button = UIBarButtonItem(title: "Done", style: .done) { _ in
    // Do stuff here.            
}

I feel like that looks much better and far more useful. You can now call multiple functions within the closure. You get a reference to your button object, and also access to other code within the same scope.

Thats great but the title of this article mentions the plural — buttons. So let's explore how we might add a closure to as many objects as we can with the least amount of repeated code.

UIControl

As mentioned earlier, it's this class that UIButton inherits from. There's actually quite a few subclasses for UIControl including UISegmentedControl, UISwitch,  UISlider, and it's also from this class that you should be creating your custom controls. Adding the closure extension at this point would cover all of its subclasses.

So let's do that!

extension UIControl {
    
    /// Typealias for UIControl closure.
    public typealias UIControlTargetClosure = (UIControl) -> ()
    
    private class UIControlClosureWrapper: NSObject {
        let closure: UIControlTargetClosure
        init(_ closure: @escaping UIControlTargetClosure) {
            self.closure = closure
        }
    }
    
    private struct AssociatedKeys {
        static var targetClosure = "targetClosure"
    }
    
    private var targetClosure: UIControlTargetClosure? {
        get {
            guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? UIControlClosureWrapper else { return nil }
            return closureWrapper.closure
        }
        set(newValue) {
            guard let newValue = newValue else { return }
            objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, UIControlClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    @objc func closureAction() {
        guard let targetClosure = targetClosure else { return }
        targetClosure(self)
    }
    
    public func addAction(for event: UIControl.Event, closure: @escaping UIControlTargetClosure) {
        targetClosure = closure
        addTarget(self, action: #selector(UIControl.closureAction), for: event)
    }
    
}

This code is almost identical to what we created for UIBarButtonItem, with just a few names changed around. We also removed the convenience initialiser, and created a new addAction function that accepts a UIControl.Event type.

This is how you use it.

let button = UIButton()
button.addAction(for: .touchUpInside) { (button) in
    // Run code
}

let slider = UISlider()
slider.addAction(for: .valueChanged) { (slider) in
    // Run code
}

let segmentedControl = UISegmentedControl()
segmentedControl.addAction(for: .valueChanged) { (segmentedControl) in
    // Run code
}

I'd love to replace the UIControl return type for the closure with a generic. So for addAction on UIButton we would have access to a UIButton in the closure, and not UIControl. You don't need this though. In fact, you could clean things up even further by removing the type from the closure altogether. In most cases you'll have access to the control anyway, in the above example, it's local. In most code bases, these controls will be properties on the class, and therefore accessible from anywhere. Instead, I've gone with a similar approach to how I believe Apple would have done it.