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 5 of 6 of this article. Click here to view the first page.

Design Pattern: Strategy

I’m happy because I’m eating a piece of pie while writing this tutorial. Perhaps that’s why it was imperative to add circles to the game. :]

You should be happy because you’ve done a great job using design patterns to refactor your game code so that it’s easy to expand and maintain.

ragecomic3

Speaking of pie, err, Pi, how do you get those circles back in your game? Right now your GameViewController can use either circles or squares, but only one or the other. It doesn’t have to be all restrictive like that.

Next, you’ll use the Strategy design pattern to manage which shapes your game produces.

The Strategy design pattern allows you to design algorithm behaviors based on what your program determines at runtime. In this case, the algorithm will choose which shapes to present to the player.

You can design many different algorithms: one that picks shapes randomly, one that picks shapes to challenge the player or help him be more successful, and so on. Strategy works by defining a family of algorithms through abstract declarations of the behavior that each strategy must implement. This makes the algorithms within the family interchangeable.

If you guessed that you’re going to implement the Strategy as a Swift protocol, you guessed correctly!

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

import Foundation

// 1
protocol TurnStrategy {
  func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView)
}

// 2
class BasicTurnStrategy: TurnStrategy {
  let shapeFactory: ShapeFactory
  let shapeViewBuilder: ShapeViewBuilder

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

  func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {
    return shapeViewBuilder.buildShapeViewsForShapes(shapeFactory.createShapes())
  }
}

class RandomTurnStrategy: TurnStrategy {
  // 3
  let firstStrategy: TurnStrategy
  let secondStrategy: TurnStrategy

  init(firstStrategy: TurnStrategy, secondStrategy: TurnStrategy) {
    self.firstStrategy = firstStrategy
    self.secondStrategy = secondStrategy
  }

  // 4
  func makeShapeViewsForNextTurnGivenPastTurns(pastTurns: [Turn]) -> (ShapeView, ShapeView) {
    if Utils.randomBetweenLower(0.0, andUpper: 100.0) < 50.0 {
      return firstStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns)
    } else {
      return secondStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns)
    }
  }
}

Here's what your new TurnStrategy does line-by-line:

  1. Declare the behavior of the algorithm. This is defined in a protocol, with one method. The method takes an array of the past turns in the game, and returns the shape views to display for the next turn.
  2. Implement a basic strategy that uses a ShapeFactory and ShapeViewBuilder. This strategy implements the existing behavior, where the shape views just come from the single factory and builder as before. Notice how you're using Dependency Injection again here, and that means this strategy doesn't care which factory or builder it's using.
  3. Implement a random strategy which randomly uses one of two other strategies. You've used composition here so that RandomTurnStrategy can behave like two potentially different strategies. However, since it's a Strategy, that composition is hidden from whatever code uses RandomTurnStrategy.
  4. This is the meat of the random strategy. It randomly selects either the first or second strategy with a 50 percent chance.

Now you need to use your strategies. Open TurnController.swift, and replace its contents with the following:

import Foundation

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

  // 1
  init(turnStrategy: TurnStrategy) {
    self.turnStrategy = turnStrategy
  }

  func beginNewTurn() -> (ShapeView, ShapeView) {
    // 2
    let shapeViews = turnStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns)
    currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])
    return shapeViews
  }

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

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

    return scoreIncrement
  }

  private let turnStrategy: TurnStrategy
}

Here's what's happening, section by section:

  1. Accepts a passed strategy and stores it on the TurnController instance.
  2. Uses the strategy to generate the ShapeView objects so the player can begin a new turn.

Note: This will cause a syntax error in GameViewController.swift. Don't worry, it's only temporary. You're going to fix the error in the very next step.

Your last step to use the Strategy design pattern is to adapt your GameViewController to use your TurnStrategy.

Open GameViewController.swift and replace its contents with the following:

import UIKit

class GameViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    // 1
    let squareShapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
    let squareShapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
    let squareShapeViewBuilder = shapeViewBuilderForFactory(squareShapeViewFactory)
    let squareTurnStrategy = BasicTurnStrategy(shapeFactory: squareShapeFactory, shapeViewBuilder: squareShapeViewBuilder)

    // 2
    let circleShapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())
    let circleShapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)
    let circleShapeViewBuilder = shapeViewBuilderForFactory(circleShapeViewFactory)
    let circleTurnStrategy = BasicTurnStrategy(shapeFactory: circleShapeFactory, shapeViewBuilder: circleShapeViewBuilder)

    // 3
    let randomTurnStrategy = RandomTurnStrategy(firstStrategy: squareTurnStrategy, secondStrategy: circleTurnStrategy)

    // 4
    turnController = TurnController(turnStrategy: randomTurnStrategy)

    beginNextTurn()
  }

  override func prefersStatusBarHidden() -> Bool {
    return true
  }

  private func shapeViewBuilderForFactory(shapeViewFactory: ShapeViewFactory) -> ShapeViewBuilder {
    let shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)
    shapeViewBuilder.fillColor = UIColor.brownColor()
    shapeViewBuilder.outlineColor = UIColor.orangeColor()
    return shapeViewBuilder
  }

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

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

    gameView.addShapeViews(shapeViews)
  }

  private var gameView: GameView { return view as! GameView }
  private var turnController: TurnController!
}

Your revised GameViewController uses TurnStrategy as follows:

  1. Create a strategy to create squares.
  2. Create a strategy to create circles.
  3. Create a strategy to randomly select either your square or circle strategy.
  4. Create your turn controller to use the random strategy.

Build and run, then go ahead and play five or six turns. You should see something similar to the following screenshots.

Screenshot111213_Animatedv2

Notice how your game randomly alternates between square shapes and circle shapes. At this point, you could easily add a third shape like triangle or parallelogram and your GameViewController could use it simply by switching up the strategy.

Design Patterns: Chain of Responsibility, Command and Iterator

Think about the example at the beginning of this tutorial:

var collection = ...

// The for loop condition uses the Iterator design pattern
for item in collection {
  println("Item is: \(item)")
}

What is it that makes the for item in collection loop work? The answer is Swift's SequenceType.

By using the Iterator pattern in a for ... in loop, you can iterate over any type that conforms to the SequenceType protocol.

The built-in collection types Array and Dictionary already conform to SequenceType, so you generally don't need to think about SequenceType unless you code your own collections. Still, it's nice to know. :]

Another design pattern that you'll often see used in conjunction with Iterator is the Command design pattern, which captures the notion of invoking a specific behavior on a target when asked.

For this tutorial, you'll use Command to determine if a Turn was a match, and compute your game's score from that.

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

import Foundation

// 1
protocol Scorer {
  func computeScoreIncrement<S: SequenceType where Turn == S.Generator.Element>(pastTurnsReversed: S) -> Int
}

// 2
class MatchScorer: Scorer {
  func computeScoreIncrement<S : SequenceType where Turn == S.Generator.Element>(pastTurnsReversed: S) -> Int {
    var scoreIncrement: Int?
    // 3
    for turn in pastTurnsReversed {
      if scoreIncrement == nil {
      	// 4
        scoreIncrement = turn.matched! ? 1 : -1
        break
      }
    }

    return scoreIncrement ?? 0
  }
}

Taking each section in turn:

  1. Define your Command type, and declare its behavior to accept a collection of past turns that you can iterate over using the Iterator design pattern.
  2. Declare a concrete implementation of Scorer that will score turns based on whether they matched or not.
  3. Use the Iterator design pattern to iterate over past turns.
  4. Compute the score as +1 for a matched turn and -1 for a non-matched turn.

Now open TurnController.swift and add the following line near the end, just before the closing brace:

private let scorer: Scorer

Then add the following line to the end of the initializer init(turnStrategy:):

self.scorer = MatchScorer()

Finally, replace the line in endTurnWithTappedShape that declares and sets scoreIncrement with the following:

var scoreIncrement = scorer.computeScoreIncrement(pastTurns.reverse())

Take note of how how you reverse pastTurns before passing it to the scorer because the scorer expects turns in reverse order (newest first), whereas pastTurns stores oldest-first (In other words, it appends newer turns to the end of the array).

Build and run your code. Did you notice something strange? I bet your scoring didn't change for some reason.

You need to make your scoring change by using the Chain of Responsibility design pattern.

The Chain of Responsibility design pattern captures the notion of dispatching multiple commands across a set of data. For this exercise, you'll dispatch different Scorer commands to compute your player's score in multiple additive ways.

For example, not only will you award +1 or -1 for matches or mismatches, but you'll also award bonus points for streaks of consecutive matches. Chain of Responsibility allows you add a second Scorer implementation in a manner that doesn't interrupt your existing scorer.

Open Scorer.swift and add the following line to the top of MatchScorer

var nextScorer: Scorer? = nil

Then add the following line to the end of the Scorer protocol:

var nextScorer: Scorer? { get set }

Now both MatchScorer and any other Scorer implementations declare that they implement the Chain of Responsibility pattern through their nextScorer property.

Replace the return statement in computeScoreIncrement with the following:

return (scoreIncrement ?? 0) + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)

Now you can add another Scorer to the chain after MatchScorer, and its score gets automatically added to the score computed by MatchScorer.

Note: The ?? operator is Swift's nil coalescing operator. It unwraps an optional to its value if non-nil, else returns the other value if the optional is nil. Effectively, a ?? b is the same as a != nil ? a! : b. It's a nice shorthand and I encourage you to use it in your code.

To demonstrate this, open Scorer.swift and add the following code to the end of the file:

class StreakScorer: Scorer {
  var nextScorer: Scorer? = nil

  func computeScoreIncrement<S : SequenceType where Turn == S.Generator.Element>(pastTurnsReversed: S) -> Int {
    // 1
    var streakLength = 0
    for turn in pastTurnsReversed {
      if turn.matched! {
        // 2
        ++streakLength
      } else {
        // 3
        break
      }
    }

    // 4
    let streakBonus = streakLength >= 5 ? 10 : 0
    return streakBonus + (nextScorer?.computeScoreIncrement(pastTurnsReversed) ?? 0)
  }
}

Your nifty new StreakScorer works as follows:

  1. Track streak length as the number of consecutive turns with successful matches.
  2. If a turn is a match, the streak continues.
  3. If a turn is not a match, the streak is broken.
  4. Compute the streak bonus: 10 points for a streak of five or more consecutive matches!

To complete the Chain of Responsibility open TurnController.swift and add the following line to the end of the initializer init(turnStrategy:):

self.scorer.nextScorer = StreakScorer()

Excellent, you're using Chain of Responsibility.

Build and run. After five successful matches in the first five turns you should see something like the following screenshot.

ScreenshotStreakAnimated

Notice how the score hits 15 after only 5 turns since 15 = 5 points for successful 5 matches + 10 points streak bonus.