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 .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Intermediate Design Patterns in Swift
50 mins
- Getting Started
- Understanding the Game
- Why Use Design Patterns?
- Design Pattern: Abstract Factory
- Design Pattern: Servant
- Leveraging Abstract Factory for Gameplay Versatility
- Design Pattern: Builder
- Design Pattern: Dependency Injection
- Design Pattern: Strategy
- Design Patterns: Chain of Responsibility, Command and Iterator
- Where To Go From Here?
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.
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:
- 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.
- Implement a basic strategy that uses a
ShapeFactory
andShapeViewBuilder
. 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. - 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 aStrategy
, that composition is hidden from whatever code usesRandomTurnStrategy
. - 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:
- Accepts a passed strategy and stores it on the
TurnController
instance. - 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.
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:
- Create a strategy to create squares.
- Create a strategy to create circles.
- Create a strategy to randomly select either your square or circle strategy.
- 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.
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:
- 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.
- Declare a concrete implementation of
Scorer
that will score turns based on whether they matched or not. - Use the Iterator design pattern to iterate over past turns.
- 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.
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:
- Track streak length as the number of consecutive turns with successful matches.
- If a turn is a match, the streak continues.
- If a turn is not a match, the streak is broken.
- 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.
Notice how the score hits 15 after only 5 turns since 15 = 5 points for successful 5 matches + 10 points streak bonus.