Elegant Controls In Swift With Closures

Elegant Controls In Swift With Closures

In yesterday's tutorial, I showed you how to use closures as an alternative to delegate protocols. Closures—or blocks in Objective-C—are incredibly useful and I frequently use them as an alternative to existing design patterns. One such pattern is the target-action pattern.

What Is It?

The target-action pattern is one of the most common Cocoa design patterns.

Target-action is a design pattern in which an object holds the information necessary to send a message to another object when an event occurs. — iOS Developer Library

The sending object keeps a reference to the message it needs to send and the object it needs to send the message to. That is how buttons and other Cocoa controls work, for example. Take a look at the following example.

import UIKit

class ViewController: UIViewController {

    @IBOutlet var button: UIButton!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        button.addTarget(self, action: #selector(didTapButton(_:)), forControlEvents: .TouchUpInside)
    }

    // MARK: - Actions

    @IBAction func didTapButton(sender: UIButton) {

    }

}

In this example, button is the sender and self, a ViewController instance, is the target or receiver. When the button detects a .TouchUpInside event, it sends a didTapButton(_:) message to the view controller. The result is that the didTapButton(_:) action is invoked on the view controller.

The target-action pattern is a transparent mechanism for sending messages to an object when a particular event takes place. This works fine and it has been working fine for years and years. The target-action pattern isn't always the best solution to a problem, though. In some situations you want to have access to the context in which the target was added. Take a look at the following example to better understand what I mean.

import UIKit

class TableViewController: UITableViewController {

    let tableViewCell = "TableViewCell"

    let items = [ "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty" ]

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Table View Data Source Methods

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCellWithIdentifier(tableViewCell, forIndexPath: indexPath) as? TableViewCell {
            // Fetch Item
            let item = items[indexPath.row]

            // Configure Cell
            cell.mainLabel.text = item
            cell.button.addTarget(self, action: #selector(didTapButton(_:)), forControlEvents: .TouchUpInside)

            return cell
        }

        return UITableViewCell(style: .Default, reuseIdentifier: nil)
    }

    // MARK: - Actions

    func didTapButton(sender: UIButton) {
        // Fetch Item
        if let superview = sender.superview, let cell = superview.superview as? TableViewCell {
            if let indexPath = tableView.indexPathForCell(cell) {
                let item = items[indexPath.row]
                print(item)
            }
        }
    }

}

This example is a bit more complex, but it better shows the goal of this tutorial. The TableViewController class is a UITableViewController subclass. In tableView(_:cellForRowAtIndexPath:), we dequeue a table view cell of type TableViewCell. This class has a button property of type UIButton. We apply the target-action pattern to enable the table view controller to respond to user interaction by adding self, the TableViewController instance, as a target to the button.

When the user taps the button of the table view cell, the didTapButton(_:) method is invoked on the TableViewController instance. In the didTapButton(_:) action, we obtain a reference to the table view cell of the button. The code we need to accomplish this isn't pretty and it can easily break if Apple decides to refactor the view hierarchy of the UITableViewCell class in a future version of UIKit.

Bad Solutions

There are a number of ways to solve this problem. We could keep a reference to the item in the table view cell. But that would go against the MVC pattern we discussed in July.

Another option is to create a UIButton subclass and have the subclass keep a reference to the table view cell it belongs to. Yuk. That sounds like a dirty solution.

Using Closures

Subclassing UIButton does open some options, though. The solution I have come to appreciate most makes use of closures, blocks in Objective-C. Let me show you how it works.

We create a UIButton subclass that declares a closure as a stored property. The closure is executed when the button detects the .TouchUpInside event. This is what the implementation of the Button class looks like.

import UIKit

class Button: UIButton {

    typealias DidTapButton = (Button) -> ()

    var didTouchUpInside: DidTapButton? {
        didSet {
            if didTouchUpInside != nil {
                addTarget(self, action: #selector(didTouchUpInside(_:)), forControlEvents: .TouchUpInside)
            } else {
                removeTarget(self, action: #selector(didTouchUpInside(_:)), forControlEvents: .TouchUpInside)
            }
        }
    }

    // MARK: - Actions

    func didTouchUpInside(sender: UIButton) {
        if let handler = didTouchUpInside {
            handler(self)
        }
    }

}

The didTouchUpInside property is of type DidTapButton?, an optional. DidTapButton is nothing more than a type alias. Note that the didTouchUpInside property defines a didSet property observer in which the button adds or removes itself as a target for the .TouchUpInside event. In other words, when the user taps the button, the button sends a message to itself. Isn't that odd?

It isn't if you look at the implementation of the didTouchUpInside(_:) action. In this action, we invoke the closure stored in didTouchUpInside, passing in the button as an argument. This is nice, but it gets even better.

Before we revisit the TableViewController class, I want to show you what the TableViewCell class looks like. It defines three outlets. The smartButton property is of type Button!. In prepareForReuse(), we set the didTouchUpInside property of smartButton to nil. It is good practice to clean up loose ends.

import UIKit

class TableViewCell: UITableViewCell {

    @IBOutlet var mainLabel: UILabel!

    @IBOutlet var button: UIButton!
    @IBOutlet var smartButton: Button!

    override func prepareForReuse() {
        smartButton.didTouchUpInside = nil
    }

}

The updated implementation of tableView(_:cellForRowAtIndexPath:) shows the benefits of the solution we implemented. We set the didTouchUpInside property of the table view cell's smartButton property. Because the closure captures the value of the item constant, we have access to its value at the moment the user taps the button, no need to jump through hoops.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    if let cell = tableView.dequeueReusableCellWithIdentifier(tableViewCell, forIndexPath: indexPath) as? TableViewCell {
        // Fetch Item
        let item = items[indexPath.row]

        // Configure Cell
        cell.mainLabel.text = item
        cell.button.addTarget(self, action: #selector(didTapButton(_:)), forControlEvents: .TouchUpInside)

        cell.smartButton.didTouchUpInside = { (sender) in
            print(item)
        }

        return cell
    }

    return UITableViewCell(style: .Default, reuseIdentifier: nil)
}

I hope you can appreciate the elegance of this solution. It is true that we created a subclass to implement this solution, that is a cost I am willing to pay.

Once you start using this pattern, it is tempting to use it everywhere. You could, for example, subclass UIView and define a closure on the subclass. That closure could then be executed in the view's drawRect(_:) method, making it very easy to customize views using a single UIView subclass. It is up to you to decide whether that is useful or not.

I prefer to keep implementation details that define the private behavior of a class or structure private, within the class or structure. The Button class exposes an implementation detail to make working with the class easier and more transparent. It doesn't expose the implementation of the class itself.

Smart Table View & Collection View Cells

Another approach I frequently use is smart table view and collection view cells. Instead of subclassing a control, such as UIButton or UISwitch, I subclass UITableViewCell or UICollectionViewCell. I apply the same technique as I described above. The difference is that the property for the closure is added to the table view or collection view cell.

This is very useful if you are creating a table view or collection view with one or more controls, such as text fields, switches, checkboxes, buttons, etc. Give it a try. You will be surprised by how easy and convenient it is.

What's Next?

It is a good exercise to play with this pattern to see where and how it can be applied. Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of this lesson from GitHub.