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: 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:
- Store configuration to set
ShapeView
fill properties. - Store configuration to set
ShapeView
outline properties. - Initialize the builder to hold a
ShapeViewFactory
to construct the views. This means the builder doesn’t need to know if it’s buildingSquareShapeView
orCircleShapeView
or even some other kind of shape view. - This is the public API; it creates and initializes a pair of
ShapeView
when there’s a pair ofShape
. - 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:
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.
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:
- Store the shapes that the player saw during the turn, and also whether the turn was a match or not.
- 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:
- Stores both the current turn and past turns.
- Accepts a
ShapeFactory
andShapeViewBuilder
. - Uses this factory and builder to create shapes and views for each new turn and records the current turn.
- 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:
- Asks the
TurnController
to begin a new turn and return a tuple ofShapeView
to use for the turn. - Informs the turn controller that the turn is over when the player taps a
ShapeView
, and then it increments the score. Notice howTurnController
abstracts score calculation away, further simplifyingGameViewController
. - 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:
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.