4.
Delegation Pattern
Written by Joshua Greene
The delegation pattern enables an object to use another “helper” object to provide data or perform a task rather than do the task itself. This pattern has three parts:
-
An object needing a delegate, also known as the delegating object. It’s the object that has a delegate. The delegate is usually held as a weak property to avoid a retain cycle where the delegating object retains the delegate, which retains the delegating object.
-
A delegate protocol, which defines the methods a delegate may or should implement.
-
A delegate, which is the helper object that implements the delegate protocol.
By relying on a delegate protocol instead of a concrete object, the implementation is much more flexible: any object that implements the protocol can be used as the delegate!
When should you use it?
Use this pattern to break up large classes or create generic, reusable components. Delegate relationships are common throughout Apple frameworks, especially UIKit. Both DataSource- and Delegate-named objects actually follow the delegation pattern, as each involves one object asking another to provide data or do something.
Why isn’t there just one protocol, instead of two, in Apple frameworks?
Apple frameworks commonly use the term DataSource to group delegate methods that provide data. For example, UITableViewDataSource is expected to provide UITableViewCells to display.
Apple frameworks typically use protocols named Delegate to group methods that receive data or events. For example, UITableViewDelegate is notified whenever a row is selected.
It’s common for the dataSource and delegate to be set to the same object, such as the view controller that owns a UITableView. However, they don’t have to be, and it can be very beneficial at times to have them set to different objects.
Playground example
Let’s take a look at some code!
Open FundamentalDesignPatterns.xcworkspace in the Starter directory and then open the Overview page, if it’s not already. You’ll see that Delegation is listed under Behavioral Patterns. This is because delegation is all about one object communicating with another object.
Click on the Delegation link to open that page.
For the code example, you’ll create a MenuViewController that has a tableView and acts as both the UITableViewDataSource and UITableViewDelegate.
First, create the MenuViewController class by adding the following code directly after Code Example, ignoring any compiler errors for the moment:
import UIKit
public class MenuViewController: UIViewController {
// 1
@IBOutlet public var tableView: UITableView! {
didSet {
tableView.dataSource = self
tableView.delegate = self
}
}
// 2
private let items = ["Item 1", "Item 2", "Item 3"]
}
Here’s what this does:
-
In a real app, you’d also need to set the
@IBOutletfor thetableViewwithin Interface Builder, or create the table view in code. You can optionally also set thetableView.delegateandtableView.dataSourcedirectly in Interface Builder, or you can do this in code as shown here. -
The
itemswill be used as the menu titles displayed on the table view.
As Xcode is likely complaining, you actually need to make MenuViewController conform to UITableViewDataSource and UITableViewDelegate.
Add the following code below the class definition:
// MARK: - UITableViewDataSource
extension MenuViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell =
tableView.dequeueReusableCell(withIdentifier: "Cell",
for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
public func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return items.count
}
}
// MARK: - UITableViewDelegate
extension MenuViewController: UITableViewDelegate {
public func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
// To do next....
}
}
Both the UITableViewDataSource and UITableViewDelegate are technically delegate protocols: They define methods that a “helper” object must implement.
It’s easy to create your own delegates too. For example, you can create a delegate to be notified whenever a user selects a menu item.
Add the following code below import UIKit:
public protocol MenuViewControllerDelegate: class {
func menuViewController(
_ menuViewController: MenuViewController,
didSelectItemAtIndex index: Int)
}
Next, add the following property right above @IBOutlet var tableView:
public weak var delegate: MenuViewControllerDelegate?
The common convention in iOS is to set delegate objects after an object is created. This is exactly what you do here: after MenuViewController is created (however this may happen in the app), it expects that its delegate property will be set.
Lastly, you need to actually inform this delegate whenever the user selects an item.
Replace the // To do next... comment in the UITableViewDelegate extension with the following:
delegate?.menuViewController(self,
didSelectItemAtIndex: indexPath.row)
It’s common convention to pass the delegating object, which in this case is the MenuViewController, to each of its delegate method calls. This way, the delegate can use or inspect the caller if needed.
So now you have created your own delegate protocol, to which the MenuViewController delegates when an item in the list is selected. In a real app, this would handle what to do when the item is selected, such as moving to a new screen.
Easy, right?
What should you be careful about?
Delegates are extremely useful, but they can be overused. Be careful about creating too many delegates for an object.
If an object needs several delegates, this may be an indicator that it’s doing too much. Consider breaking up the object’s functionality for specific use cases, instead of one catch-all class.
It’s hard to put a number on how many is too many; there’s no golden rule. However, if you find yourself constantly switching between classes to understand what’s happening, then that’s a sign you have too many. Similarly, if you cannot understand why a certain delegate is useful, then that’s a sign it’s too small, and you’ve split things up too much.
You should also be careful about creating retain cycles. Most often, delegate properties should be weak. If an object must absolutely have a delegate set, consider adding the delegate as an input to the object’s initializer and marking its type as forced unwrapped using ! instead of optional via ?. This will force consumers to set the delegate before using the object.
If you find yourself tempted to create a strong delegate, another design pattern may be better suited for your use case. For example, you might consider using the strategy pattern instead. See Chapter 5 for more details.
Tutorial project
The playground example has given you a small taste for what it looks like to implement the delegation pattern. It’s now time to take that theory and make use of it in an app. You’ll continue the RabbleWabble app from the previous chapter, and add a menu controller to select the group of questions.
If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter, and then open starter\RabbleWabble\RabbleWabble.xcodeproj in Xcode.
Instead of just showing the basic phrases questions, you’ll create a new view controller to let the user select from a list of question group options.
In the File hierarchy, right-click on Controllers and select New File. Select the iOS tab, pick Swift File from the list, and click Next. Enter SelectQuestionGroupViewController.swift for the file name and click Create.
Replace the contents of the SelectQuestionGroupViewController.swift with the following:
import UIKit
public class SelectQuestionGroupViewController: UIViewController {
// MARK: - Outlets
@IBOutlet internal var tableView: UITableView! {
didSet {
tableView.tableFooterView = UIView()
}
}
// MARK: - Properties
public let questionGroups = QuestionGroup.allGroups()
private var selectedQuestionGroup: QuestionGroup!
}
You’ll use the tableView to display a list of question groups. Whenever the tableView is set, you set tableView.tableFooterView to a blank UIView. This trick is to prevent the table view from drawing unnecessary empty table view cells, which it does by default after all the other cells are drawn.
You set questionGroups to QuestionGroup.allGroups(), which is a convenience method provided by the extension defined in QuestionGroupData.swift that simply returns all of the possible QuestionGroup options.
You’ll later use selectedQuestionGroup to hold onto whichever QuestionGroup the user selects.
Next, you need to make SelectQuestionGroupViewController conform to UITableViewDataSource to display the table view cells. Add the following extension to the end of the file:
// MARK: - UITableViewDataSource
extension SelectQuestionGroupViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int {
return questionGroups.count
}
public func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
return UITableViewCell()
}
}
For now, you simply return an empty UITableViewCell from tableView(_:, cellForRowAt:) as a placeholder.
In order to actually implement this, you need a custom UITableViewCell subclass. This will allow you to completely control the cell’s look and feel. In the File hierarchy, right-click on Views and select New File.
Select the iOS tab, pick Swift File from the list, and click Next. Enter QuestionGroupCell.swift for the file name and click Create.
Replace the contents of QuestionGroupCell.swift with the following:
import UIKit
public class QuestionGroupCell: UITableViewCell {
@IBOutlet public var titleLabel: UILabel!
@IBOutlet public var percentageLabel: UILabel!
}
You’ll create this view and connect the outlets soon, but for now, open SelectQuestionGroupViewController.swift again.
Replace the existing tableView(_:, cellForRowAt:) with the following:
public func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "QuestionGroupCell") as! QuestionGroupCell
let questionGroup = questionGroups[indexPath.row]
cell.titleLabel.text = questionGroup.title
return cell
}
Build and run to make sure you don’t have any compiler warnings. You shouldn’t see anything different just yet, however, as you haven’t actually added SelectQuestionGroupViewController to the app. You’ll do this next.
Setting up the views
Open Main.storyboard, select the Object library button and enter UIViewController into the search field in the new window that appears.
Hold the Option key to prevent the window from closing and drag and drop a new View Controller to the left of the existing scene.
Next, enter UITableView into the search field on the Object library window, and drag and drop a new Table View onto the new view controller.
Select the table view, then select the Add New Constraints icon, and do the following:
- Set the top constraint to 0.
- Set the leading constraint to 0.
- Set the trailing constraint to 0.
- Set the bottom constraint to 0.
- Uncheck constrain to margins.
- Press Add 4 Constraints.
Enter UITableViewCell into the search field on the Object library window, and drag and drop a Table View Cell onto the table view.
Lastly, enter label into the search field on the Object library window, and drag two new labels onto the table view cell. Then, press the red X on the Object library window to close it.
Double-click the first label and set its text to Title. Position it to the far left of the cell aligned with the top and left margins (it should show blue indicators).
Double-click the second label and set its text to 0%. Position it to the far right of the cell aligned with the top and right margins.
Your scene should now look like this:
You now need to set constraints on the labels.
Select the Title label, then select the Add New Constraints icon, and do the following:
- Set the top constraint to 0.
- Set the leading constraint to 0.
- Set the trailing constraint to 8.
- Set the bottom constraint to 0.
- Verify constrain to margins is checked.
- Press Add 4 Constraints.
Select the 0% label, then select the Add New Constraints icon and do the following:
- Set the top constraint to 0.
- Set the trailing constraint to 0.
- Set the bottom constraint to 0.
- Verify constrain to margins is checked.
- Press Add 3 Constraints.
Lastly, select the Percent label, go to the Size Inspector, scroll down to Content Hugging Priority and set Horizontal to 750.
Great! You’ve got the views all set up. You next need to set the class identity, reuse identifier and hookup IBOutlets.
Select the table view cell, go to the Identity Inspector and set the Class to QuestionGroupCell.
With the cell still selected, switch to the Attributes Inspector and set the Identifier to QuestionGroupCell.
Switch to the Connections Inspector, and drag the titleLabel outlet on to the Title label and the percentageLabel on to the 0% label.
Next, select the yellow view controller object on the scene, go to the Identity Inspector and set the Class to SelectQuestionGroupViewController.
With the SelectQuestionGroupViewController still selected, go to the Connections Inspector, then drag and drop the tableView outlet onto the table view in the scene.
Next, select the table view in the scene, go to the Connections Inspector and drag and drop both the dataSource and delegate outlets onto the yellow view controller object.
In order to show the SelectQuestionGroupViewController whenever the app is opened, you need to set it as the Initial View Controller.
To do so, drag and drop the Arrow currently pointing at the QuestionViewController scene to point at the SelectQuestionGroupViewController scene instead.
It should look like this:
Build and run to see the question groups displayed on the table view. Sweet!
Tap on a cell, however, and the app does nothing. Your next job is to fix this.
Displaying selected question groups
Open Main.storyboard again and select the SelectQuestionGroupViewController scene. Press the Editor menu button, and then Embed In ▸ Navigation Controller.
Click on the newly-added navigation bar on the SelectQuestionGroupViewController scene to select the Navigation Item, then go to the Attributes Inspector and set the Title to Select Question Group.
You next need to create a segue to the QuestionViewController scene.
To do so, select the QuestionGroupCell, then Control-drag and drop it onto the QuestionViewController scene.
Select Show in the popup window that appears. This creates a segue that will be triggered whenever the user taps a table view cell.
Build and run and try clicking on the first table view cell. Awesome; you can see questions!
Press the back button and try clicking on the second cell. Oh wait… are those the same questions?! Yes, they are indeed. You need to set the selected QuestionGroup on the QuestionViewController. To do so, you’ll need to make SelectQuestionGroupViewController conform to UITableViewDelegate to be notified of taps on the table view.
Open SelectQuestionGroupViewController.swift and add the following extension to the end of the file:
// MARK: - UITableViewDelegate
extension SelectQuestionGroupViewController: UITableViewDelegate {
// 1
public func tableView(_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath)
-> IndexPath? {
selectedQuestionGroup = questionGroups[indexPath.row]
return indexPath
}
// 2
public func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
// 3
public override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
guard let viewController = segue.destination
as? QuestionViewController else { return }
viewController.questionGroup = selectedQuestionGroup
}
}
Here’s what each of these methods is doing:
-
This sets the
selectedQuestionGroupto the one that was selected. You have to do this here instead of intableView(_:, didSelectRowAt:), becausedidSelectRowAt:is triggered after the segue is performed. If you setselectedQuestionGroupindidSelectRowAt:then the app would crash on the lineviewController.questionGroup = selectedQuestionGroupasselectedQuestionGroupwould still benil. -
Within
tableView(_:, didSelectRowAt:), you simply deselect the table view cell. This is just a nicety so you won’t see any selected cells should you return to this view controller later. -
Within
prepare(for:, sender:), you guard that thesegue.destinationis actually aQuestionViewController(just in case!), and if so, you set itsquestionGroupto theselectedQuestionGroup.
Build and run and try selecting the first and then the second table view cells as you did before to verify its working as expected.
Great job!
Creating a custom delegate
The app is starting to come along, but there’s still a few things missing:
-
Wouldn’t it be nice to actually show the title of the question group on the
QuestionViewController? You bet it would! -
You also can’t see how many questions are remaining in the
QuestionViewController. It’d be great if this showed up! -
Furthermore, if you click through all of the questions in the
QuestionViewController(by pressing either the green check or red X buttons), nothing happens at the end. It’d be nice if something happened! -
Lastly, it’s common convention for a “presented” controller to notify its caller, typically via a delegate, whenever “Cancel” is pressed. There’s no option to cancel at the moment, but there is a back button. It’d be great to replace this with a custom bar button item instead!
While it sounds like a bit of work, all of these are actually just a few lines of coding. You can do it!
To resolve the first issue, open QuestionViewController and replace:
public var questionGroup = QuestionGroup.basicPhrases()
with the following:
public var questionGroup: QuestionGroup! {
didSet {
navigationItem.title = questionGroup.title
}
}
Build and run, and voila, the title shows on the navigation bar!
To resolve the second issue, add the following right after the other properties:
private lazy var questionIndexItem: UIBarButtonItem = {
let item = UIBarButtonItem(title: "",
style: .plain,
target: nil,
action: nil)
item.tintColor = .black
navigationItem.rightBarButtonItem = item
return item
}()
Finally, add the following line to the end of showQuestion():
questionIndexItem.title = "\(questionIndex + 1)/" +
"\(questionGroup.questions.count)"
Build and run and try clicking through the questions. Cool, right?
Addressing the last two issues is a bit trickier. You need to create a custom delegate for them. Fortunately, this is also pretty easy to do.
Add the following to the top of QuestionViewController.swift, below import UIKit:
public protocol QuestionViewControllerDelegate: class {
// 1
func questionViewController(
_ viewController: QuestionViewController,
didCancel questionGroup: QuestionGroup,
at questionIndex: Int)
// 2
func questionViewController(
_ viewController: QuestionViewController,
didComplete questionGroup: QuestionGroup)
}
Here’s how you’ll use these methods:
- You’ll call
questionViewController(_:didCancel:at:)when the user presses the Cancel button, which you’ve yet to create. - You’ll call
questionViewController(_:didComplete:)when the user completes all of the questions.
You also need a property to hold onto the delegate. Add the following right below // MARK: - Instance Properties:
public weak var delegate: QuestionViewControllerDelegate?
Next, you’ll need to set this delegate. Open SelectQuestionGroupViewController.swift and add the following to the end of prepare(for:sender:):
viewController.delegate = self
This will result in a compiler error, however, as you haven’t made SelectQuestionGroupViewController conform to QuestionViewControllerDelegate yet.
To fix that, add the following extension to the end of the file:
// MARK: - QuestionViewControllerDelegate
extension SelectQuestionGroupViewController: QuestionViewControllerDelegate {
public func questionViewController(
_ viewController: QuestionViewController,
didCancel questionGroup: QuestionGroup,
at questionIndex: Int) {
navigationController?.popToViewController(self,
animated: true)
}
public func questionViewController(
_ viewController: QuestionViewController,
didComplete questionGroup: QuestionGroup) {
navigationController?.popToViewController(self,
animated: true)
}
}
For now you’ll simply pop to the SelectQuestionGroupViewController regardless of which delegate method is called.
You next need to actually call these delegate methods appropriately.
Open QuestionViewController.swift and replace viewDidLoad() with the following, ignoring the compiler error about a missing method for now:
public override func viewDidLoad() {
super.viewDidLoad()
setupCancelButton()
showQuestion()
}
Next, add the following two methods just below viewDidLoad():
private func setupCancelButton() {
let action = #selector(handleCancelPressed(sender:))
let image = UIImage(named: "ic_menu")
navigationItem.leftBarButtonItem =
UIBarButtonItem(image: image,
landscapeImagePhone: nil,
style: .plain,
target: self,
action: action)
}
@objc private func handleCancelPressed(sender: UIBarButtonItem) {
delegate?.questionViewController(
self,
didCancel: questionGroup,
at: questionIndex)
}
This sets a new Cancel button as the navigationItem.leftBarButtonItem, which calls handleCancelPressed(sender:) when it’s pressed to notify the delegate.
Build and run to try out your new rockin’ cancel button!
Finally, still in QuestionViewController.swift, scroll down and replace the // TODO: - Handle this...! comment with the following:
delegate?.questionViewController(self,
didComplete: questionGroup)
Build and run and select the “Basic Phrases” cell, since this has only a few questions. Press the red X or green check buttons until you reach the end, and check out how the app now pops back to the SelectQuestionGroupViewController. Nice!
Key points
You learned about the delegation pattern in this chapter, including how to use Apple-provided delegates and how to create your own delegates as well. Here are the key points you learned:
-
The delegation pattern has three parts: an object needing a delegate, a delegate protocol and a delegate.
-
This pattern allows you to break up large classes and create generic, reusable components.
-
Delegates should be
weakproperties in the vast majority of use cases.
RabbleWabble is starting to come along! However, there’s still a lot to do to make this the next App Store success.
Continue onto the next chapter to learn about the strategy design pattern and continue building out RabbleWabble.