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