Adding a closure as a target to UIButton and other controls in Swift

The target-action pattern is used in combination with user interface controls as a callback to a user event. Whenever a button is pressed on a target, its action will be called. The fact that the method is not defined close to the control definition is sometimes seen as a downside and reason for a lot of us developers to search for closure based solutions on sites like Stack Overflow.

The iOS 14 SDK introduced new APIs that allow us to use UIControls in combination with closures. Common UIControl elements are UIButton, UISegmentedControl, and UISwitch but there are a lot more which all inherit from the UIControl object. All those interface elements can now be used with this new API.

Using a UIControl with a closure callback

You’re most likely familiar with the following piece of code:

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton(type: .system)
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)

    }

    @IBAction func buttonTapped(_ sender: UIButton) {
        print("Button tapped!")
    }
}

The buttonTapped(_:) method will be called every time the button is tapped.

The same piece of code can now be written as follows:

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton(type: .system, primaryAction: UIAction(title: "Button Title", handler: { _ in
            print("Button tapped!")
        }))
    }
}

This keeps the action close to the control definition which can improve discoverability of your code.

Getting a reference to the control sender

One difference in the above piece of code is that our method had a reference to the sender. This could be handy in some cases when you want to know which of the controls in place called the linked method.

With the new closure based API, you can use the action argument to access the sender. For example, when you want to read out the text from a text field:

let textField = UITextField()
textField.addAction(UIAction(title: "", handler: { action in
    let textField = action.sender as! UITextField
    print("Text is \(textField.text)")
}), for: .editingChanged)

Should I always use the new closure API?

It might be tempting to now use closures everywhere. However, a control action can easily grow in code, making your code less readable. In those cases, you might want to fallback to the old target-action pattern which allows you to use a separated method.

Methods in Swift are easier to read when your control action requires multiple lines of code.

Conclusion

The new closure based API in the iOS 14 SDK is a welcome change but should be used carefully. Using many closures can easily make your code less readable and should only be used if the control callback logic can be written in a few lines of code. Otherwise, it’s better to use the old fashioned target-action pattern.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!