Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

4. Delegation Pattern
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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!

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"]
}
// 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....
  }
}
public protocol MenuViewControllerDelegate: class {
  func menuViewController(
    _ menuViewController: MenuViewController,
    didSelectItemAtIndex index: Int)
}
public weak var delegate: MenuViewControllerDelegate?
delegate?.menuViewController(self,
  didSelectItemAtIndex: indexPath.row)

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.

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.

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!
}
// 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()
  }
}
import UIKit

public class QuestionGroupCell: UITableViewCell {
  @IBOutlet public var titleLabel: UILabel!
  @IBOutlet public var percentageLabel: UILabel!
}
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
}

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.

Displaying selected question groups

Open Main.storyboard again and select the SelectQuestionGroupViewController scene. Press the Editor menu button, and then Embed In ▸ Navigation Controller.

// 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
  }
}

Creating a custom delegate

The app is starting to come along, but there’s still a few things missing:

public var questionGroup = QuestionGroup.basicPhrases()
public var questionGroup: QuestionGroup! {
  didSet {
    navigationItem.title = questionGroup.title
  }
}

private lazy var questionIndexItem: UIBarButtonItem = {
  let item = UIBarButtonItem(title: "",
                             style: .plain,
                             target: nil,
                             action: nil)
  item.tintColor = .black
  navigationItem.rightBarButtonItem = item
  return item
}()
questionIndexItem.title = "\(questionIndex + 1)/" +
"\(questionGroup.questions.count)"

public protocol QuestionViewControllerDelegate: class {
  
  // 1
  func questionViewController(
    _ viewController: QuestionViewController,
    didCancel questionGroup: QuestionGroup,
    at questionIndex: Int)

  // 2
  func questionViewController(
    _ viewController: QuestionViewController,
    didComplete questionGroup: QuestionGroup)
}
public weak var delegate: QuestionViewControllerDelegate?
viewController.delegate = self
// 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)
  }
}
public override func viewDidLoad() {
  super.viewDidLoad()
  setupCancelButton()
  showQuestion()
}
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)
}

delegate?.questionViewController(self,
                                 didComplete: questionGroup)

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now