Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

9. Builder 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 builder pattern allows you to create complex objects by providing inputs step-by-step, instead of requiring all inputs upfront via an initializer. This pattern involves three main types:

  1. The director accepts inputs and coordinates with the builder. This is usually a view controller or a helper class that’s used by a view controller.

  2. The product is the complex object to be created. This can be either a struct or a class, depending on desired reference semantics. It’s usually a model, but it can be any type depending on your use case.

  3. The builder accepts step-by-step inputs and handles the creation of the product. This is often a class, so it can be reused by reference.

When should you use it?

Use the builder pattern when you want to create a complex object using a series of steps.

This pattern works especially well when a product requires multiple inputs. The builder abstracts how these inputs are used to create the product, and it accepts them in whatever order the director wants to provide them.

For example, you can use this pattern to implement a “hamburger builder.” The product could be a hamburger model, which has inputs such as meat selection, toppings and sauces. The director could be an employee object, which knows how to build hamburgers, or it could be a view controller that accepts inputs from the user.

The “hamburger builder” can thereby accept meat selection, toppings and sauces in any order and create a hamburger upon request.

Playground example

Open FundamentalDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, and then open the Overview page.

import Foundation

// MARK: - Product
// 1
public struct Hamburger {
  public let meat: Meat
  public let sauce: Sauces
  public let toppings: Toppings
}

extension Hamburger: CustomStringConvertible {
  public var description: String {
    return meat.rawValue + " burger"
  }
}

// 2
public enum Meat: String {
  case beef
  case chicken
  case kitten
  case tofu
}

// 3
public struct Sauces: OptionSet {
  public static let mayonnaise = Sauces(rawValue: 1 << 0)
  public static let mustard = Sauces(rawValue: 1 << 1)
  public static let ketchup = Sauces(rawValue: 1 << 2)
  public static let secret = Sauces(rawValue: 1 << 3)

  public let rawValue: Int
  public init(rawValue: Int) {
    self.rawValue = rawValue
  }
}

// 4
public struct Toppings: OptionSet {
  public static let cheese = Toppings(rawValue: 1 << 0)
  public static let lettuce = Toppings(rawValue: 1 << 1)
  public static let pickles = Toppings(rawValue: 1 << 2)
  public static let tomatoes = Toppings(rawValue: 1 << 3)

  public let rawValue: Int
  public init(rawValue: Int) {
    self.rawValue = rawValue
  }
}
// MARK: - Builder
public class HamburgerBuilder {

  // 1
  public private(set) var meat: Meat = .beef
  public private(set) var sauces: Sauces = []
  public private(set) var toppings: Toppings = []

  // 2
  public func addSauces(_ sauce: Sauces) {
    sauces.insert(sauce)
  }

  public func removeSauces(_ sauce: Sauces) {
    sauces.remove(sauce)
  }

  public func addToppings(_ topping: Toppings) {
    toppings.insert(topping)
  }

  public func removeToppings(_ topping: Toppings) {
    toppings.remove(topping)
  }

  public func setMeat(_ meat: Meat) {
    self.meat = meat
  }

  // 3
  public func build() -> Hamburger {
    return Hamburger(meat: meat,
                     sauce: sauces,
                     toppings: toppings)
  }
}
private var soldOutMeats: [Meat] = [.kitten]
public enum Error: Swift.Error {
  case soldOut
}
public func setMeat(_ meat: Meat) throws {
  guard isAvailable(meat) else { throw Error.soldOut }
  self.meat = meat
}

public func isAvailable(_ meat: Meat) -> Bool {
  return !soldOutMeats.contains(meat)
}

// MARK: - Director
public class Employee {

  public func createCombo1() throws -> Hamburger {
    let builder = HamburgerBuilder()
    try builder.setMeat(.beef)
    builder.addSauces(.secret)
    builder.addToppings([.lettuce, .tomatoes, .pickles])
    return builder.build()
  }

  public func createKittenSpecial() throws -> Hamburger {
    let builder = HamburgerBuilder()
    try builder.setMeat(.kitten)
    builder.addSauces(.mustard)
    builder.addToppings([.lettuce, .tomatoes])
    return builder.build()
  }
}
// MARK: - Example
let burgerFlipper = Employee()

if let combo1 = try? burgerFlipper.createCombo1() {
  print("Nom nom " + combo1.description)
}
Nom nom beef burger
if let kittenBurger = try?
  burgerFlipper.createKittenSpecial() {
  print("Nom nom nom " + kittenBurger.description)

} else {
  print("Sorry, no kitten burgers here... :[")
}
Sorry, no kitten burgers here... :[

What should you be careful about?

The builder pattern works best for creating complex products that require multiple inputs using a series of steps. If your product doesn’t have several inputs or can’t be created step by step, the builder pattern may be more trouble than it’s worth.

Tutorial project

You’ll continue the RabbleWabble app from the previous chapter. Specifically, you’ll add the capability to create a new QuestionGroup using the builder pattern.

// MARK: - CreateQuestionGroupViewControllerDelegate
extension SelectQuestionGroupViewController: CreateQuestionGroupViewControllerDelegate {

  public func createQuestionGroupViewControllerDidCancel(
    _ viewController: CreateQuestionGroupViewController) {
    dismiss(animated: true, completion: nil)
  }

  public func createQuestionGroupViewController(
    _ viewController: CreateQuestionGroupViewController,
    created questionGroup: QuestionGroup) {

    questionGroupCaretaker.questionGroups.append(questionGroup)
    try? questionGroupCaretaker.save()

    dismiss(animated: true, completion: nil)
    tableView.reloadData()
  }
}
public override func prepare(
  for segue: UIStoryboardSegue, sender: Any?) {
  // 1
  if let viewController =
    segue.destination as? QuestionViewController {
    viewController.questionStrategy =
      appSettings.questionStrategy(for: questionGroupCaretaker)
    viewController.delegate = self

    // 2
  } else if let navController =
      segue.destination as? UINavigationController,
    let viewController =
      navController.topViewController as? CreateQuestionGroupViewController {
    viewController.delegate = self
  }

  // 3
  // Whatevs... skip anything else
}

Implementing the builder pattern

CreateQuestionGroupViewController is a new file added in this chapter. It uses a table view to accept inputs for creating a QuestionGroup. It displays CreateQuestionGroupTitleCell and CreateQuestionCell to collect input from the user.

public class QuestionBuilder {
  public var answer = ""
  public var hint = ""
  public var prompt = ""

  public func build() throws -> Question {
    guard answer.count > 0 else { throw Error.missingAnswer }
    guard prompt.count > 0 else { throw Error.missingPrompt }
    return Question(answer: answer, hint: hint, prompt: prompt)
  }

  public enum Error: String, Swift.Error {
    case missingAnswer
    case missingPrompt
  }
}
public class QuestionGroupBuilder {

  // 1
  public var questions = [QuestionBuilder()]
  public var title = ""

  // 2
  public func addNewQuestion() {
    let question = QuestionBuilder()
    questions.append(question)
  }

  public func removeQuestion(at index: Int) {
    questions.remove(at: index)
  }

  // 3
  public func build() throws -> QuestionGroup {
    guard self.title.count > 0 else {
      throw Error.missingTitle
    }
    
    guard self.questions.count > 0 else {
      throw Error.missingQuestions
    }

    let questions = try self.questions.map { try $0.build() }
    return QuestionGroup(questions: questions, title: title)
  }

  public enum Error: String, Swift.Error {
    case missingTitle
    case missingQuestions
  }
}
public let questionGroupBuilder = QuestionGroupBuilder()
return questionGroupBuilder.questions.count + 2
} else if row == 1 {
} else if row >= 1 &&
          row <= questionGroupBuilder.questions.count {
cell.titleTextField.text = questionGroupBuilder.title
private func questionBuilder(
  for indexPath: IndexPath) -> QuestionBuilder {
  
  return questionGroupBuilder.questions[indexPath.row - 1]
}
let questionBuilder = self.questionBuilder(for: indexPath)
cell.delegate = self
cell.answerTextField.text = questionBuilder.answer
cell.hintTextField.text = questionBuilder.hint
cell.indexLabel.text = "Question \(indexPath.row)"
cell.promptTextField.text = questionBuilder.prompt
public override func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath) {

  tableView.deselectRow(at: indexPath, animated: true)
  guard isLastIndexPath(indexPath) else { return }
  questionGroupBuilder.addNewQuestion()
  tableView.insertRows(at: [indexPath], with: .top)
}

private func isLastIndexPath(_ indexPath: IndexPath) -> Bool {
  return indexPath.row ==
    tableView.numberOfRows(inSection: indexPath.section) - 1
}

private func questionBuilder(
  for cell: CreateQuestionCell) -> QuestionBuilder  {
  
  let indexPath = tableView.indexPath(for: cell)!
  return questionBuilder(for: indexPath)
}
questionBuilder(for: cell).answer = text
questionBuilder(for: cell).hint = text
questionBuilder(for: cell).prompt = text
questionGroupBuilder.title = text
@IBAction func savePressed(_ sender: Any) {
  do {
    let questionGroup = try questionGroupBuilder.build()
    delegate?.createQuestionGroupViewController(
      self, created: questionGroup)

  } catch {
    displayMissingInputsAlert()
  }
}

public func displayMissingInputsAlert() {
  let alert = UIAlertController(
    title: "Missing Inputs",
    message: "Please provide all non-optional values",
    preferredStyle: .alert)

  let okAction = UIAlertAction(title: "Ok",
                               style: .default,
                               handler: nil)
  alert.addAction(okAction)
  present(alert, animated: true, completion: nil)
}

Key points

You learned the builder pattern in this chapter. Here are its key points:

Where to go from here?

RabbleWabble has really come a long way since you created it, but there’s still a lot of functionality you can add.

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