Intermediate Design Patterns in Swift

Design patterns are incredibly useful for making code maintainable and readable. Learn design patterns in Swift with this hands on tutorial. By .

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 6 of this article. Click here to view the first page.

Design Pattern: Builder

Now it’s time to examine a third design pattern: Builder.

Suppose you want to vary the appearance of your ShapeView instances — whether they should show fill and outline colors and what colors to use. The Builder design pattern makes such object configuration easier and more flexible.

One approach to solve this configuration problem would be to add a variety of constructors, either class convenience methods like CircleShapeView.redFilledCircleWithBlueOutline() or initializers with a variety of arguments and default values.

Unfortunately, it’s not a scalable technique as you’d need to write a new method or initializer for every combination.

Builder solves this problem rather elegantly because it creates a class with a single purpose — configure an already initialized object. If you set up your builder to build red circles and then later blue circles, it’ll do so without need to alter CircleShapeView.

Create a new file ShapeViewBuilder.swift and replace its contents with the following code:

import Foundation
import UIKit

class ShapeViewBuilder {
  // 1
  var showFill  = true
  var fillColor = UIColor.orangeColor()

  // 2
  var showOutline  = true
  var outlineColor = UIColor.grayColor()

  // 3
  init(shapeViewFactory: ShapeViewFactory) {
    self.shapeViewFactory = shapeViewFactory
  }

  // 4
  func buildShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
    let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes)
    configureShapeView(shapeViews.0)
    configureShapeView(shapeViews.1)
    return shapeViews
  }

  // 5
  private func configureShapeView(shapeView: ShapeView) {
    shapeView.showFill  = showFill
    shapeView.fillColor = fillColor
    shapeView.showOutline  = showOutline
    shapeView.outlineColor = outlineColor
  }

  private var shapeViewFactory: ShapeViewFactory
}

Here’s how your new ShapeViewBuilder works:

  1. Store configuration to set ShapeView fill properties.
  2. Store configuration to set ShapeView outline properties.
  3. Initialize the builder to hold a ShapeViewFactory to construct the views. This means the builder doesn’t need to know if it’s building SquareShapeView or CircleShapeView or even some other kind of shape view.
  4. This is the public API; it creates and initializes a pair of ShapeView when there’s a pair of Shape.
  5. Do the actual configuration of a ShapeView based on the builder’s stored configuration.

Deploying your spiffy new ShapeViewBuilder is as easy as opening GameViewController.swift and adding the following code to the bottom of the class, just before the closing curly brace:

private var shapeViewBuilder: ShapeViewBuilder!

Now, populate your new property by adding the following code to viewDidLoad just above the line that invokes beginNextTurn:

shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)
shapeViewBuilder.fillColor = UIColor.brownColor()
shapeViewBuilder.outlineColor = UIColor.orangeColor()

Finally replace the line that creates shapeViews in beginNextTurn with the following:

let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes)

Build and run, and you should see something like this:

Screenshot8

Notice how your circles are now a pleasant brown with orange outlines — I know you must be amazed by the stunning design here, but please don’t try to hire me to be your interior decorator. ;]

Now to reinforce the power of the Builder pattern. With GameViewController.swift still open, change your viewDidLoad to use square factories:

shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)

Build and run, and you should see this.

Screenshot9

Notice how the Builder pattern made it easy to apply a new color scheme to squares as well as to circles. Without it, you’d need color configuration code in both CircleShapeViewFactory and SquareShapeViewFactory.

Furthermore, changing to another color scheme would involve widespread code changes. By restricting ShapeView color configuration to a single ShapeViewBuilder, you also isolate color changes to a single class.

Design Pattern: Dependency Injection

Every time you tap a shape, you’re taking a turn in your game, and each turn can be a match or not a match.

Wouldn’t it be helpful if your game could track all the turns, stats and award point bonuses for hot streaks?

Create a new file called Turn.swift, and replace its contents with the following code:

import Foundation

class Turn {
  // 1
  let shapes: [Shape]
  var matched: Bool?

  init(shapes: [Shape]) {
    self.shapes = shapes
  }

  // 2
  func turnCompletedWithTappedShape(tappedShape: Shape) {
    var maxArea = shapes.reduce(0) { $0 > $1.area ? $0 : $1.area }
    matched = tappedShape.area >= maxArea
  }
}

Your new Turn class does the following:

  1. Store the shapes that the player saw during the turn, and also whether the turn was a match or not.
  2. Records the completion of a turn after a player taps a shape.

To control the sequence of turns your players play, create a new file named TurnController.swift, and replace its contents with the following code:

import Foundation

class TurnController {
  // 1
  var currentTurn: Turn?
  var pastTurns: [Turn] = [Turn]()

  // 2
  init(shapeFactory: ShapeFactory, shapeViewBuilder: ShapeViewBuilder) {
    self.shapeFactory = shapeFactory
    self.shapeViewBuilder = shapeViewBuilder
  }

  // 3
  func beginNewTurn() -> (ShapeView, ShapeView) {
    let shapes = shapeFactory.createShapes()
    let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes)
    currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])
    return shapeViews
  }

  // 4
  func endTurnWithTappedShape(tappedShape: Shape) -> Int {
    currentTurn!.turnCompletedWithTappedShape(tappedShape)
    pastTurns.append(currentTurn!)

    var scoreIncrement = currentTurn!.matched! ? 1 : -1

    return scoreIncrement
  }

  private let shapeFactory: ShapeFactory
  private var shapeViewBuilder: ShapeViewBuilder
}

Your TurnController works as follows:

  1. Stores both the current turn and past turns.
  2. Accepts a ShapeFactory and ShapeViewBuilder.
  3. Uses this factory and builder to create shapes and views for each new turn and records the current turn.
  4. Records the end of a turn after the player taps a shape, and returns the computed score based on whether the turn was a match or not.

Now open GameViewController.swift, and add the following code at the bottom, just above the closing curly brace:

private var turnController: TurnController!

Scroll up to viewDidLoad, and just before the line invoking beginNewTurn, insert the following code:

turnController = TurnController(shapeFactory: shapeFactory, shapeViewBuilder: shapeViewBuilder)

Replace beginNextTurn with the following:

private func beginNextTurn() {
  // 1
  let shapeViews = turnController.beginNewTurn()

  shapeViews.0.tapHandler = {
    tappedView in
    // 2
    self.gameView.score += self.turnController.endTurnWithTappedShape(tappedView.shape)
    self.beginNextTurn()
  }

  // 3
  shapeViews.1.tapHandler = shapeViews.0.tapHandler

  gameView.addShapeViews(shapeViews)
}

Your new code works as follows:

  1. Asks the TurnController to begin a new turn and return a tuple of ShapeView to use for the turn.
  2. Informs the turn controller that the turn is over when the player taps a ShapeView, and then it increments the score. Notice how TurnController abstracts score calculation away, further simplifying GameViewController.
  3. Since you removed explicit references to specific shapes, the second shape view can share the same tapHandler closure as the first shape view.

An example of the Dependency Injection design pattern is that it passes in its dependencies to the TurnController initializer. The initializer parameters essentially inject the shape and shape view factory dependencies.

Since TurnController makes no assumptions about which type of factories to use, you’re free to swap in different factories.

Not only does this make your game more flexible, but it makes automated testing easier since it allows you to pass in special TestShapeFactory and TestShapeViewFactory classes if you desire. These could be special stubs or mocks that would make testing easier, more reliable or faster.

Build and run and check that it looks like this:

Screenshot10

There are no visual differences, but TurnController has opened up your code so it can use more sophisticated turn strategies: calculating scores based on streaks of turns, alternating shape type between turns, or even adjusting the difficulty of play based on the player’s performance.