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.