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?
Why Use Design Patterns?
You’re probably wondering to yourself, “Hmmm, so why do I need design patterns when I have a working game?” Well, what if you want to support shapes other than just squares?
You could add code to create a second shape in beginNextTurn
, but as you add a third, fourth or even fifth type of shape the code would become unmanageable.
And what if you want the player to be able to select the shape she plays?
If you lump all of that code together in GameViewController
you’ll end up with tightly-coupled code containing hard-coded dependencies that will be difficult to manage.
Here’s the answer to your question: design patterns help decouple your code into nicely-separated bits.
Before moving on, I have a confession; I already snuck in a design pattern. :]
[spoiler title=”Can You Spot the Design Pattern?”]The ShapeView.tapHandler
uses the Observer design pattern to inform interested code that the player tapped the view. Notice how this nicely decouples the rendering of the view from the logic to handle interactions with the view?[/spoiler]
Now, on to the design patterns. Each section from here on describes a different design pattern. Let’s get going!
Design Pattern: Abstract Factory
GameViewController
is tightly coupled with the SquareShapeView
, and that doesn’t allow much room to later use a different view to represent squares or introduce a second shape.
Your first task is to decouple and simplify your GameViewController
using the Abstract Factory design pattern. You’re going to use this pattern in code that establishes an API for constructing a group of related objects, like the shape views you’ll work with momentarily, without hard-coding specific classes.
Click File\New\File… and then select iOS\Source\Swift File. Call the file ShapeViewFactory.swift, save it and then replace its contents with the code below:
import Foundation
import UIKit
// 1
protocol ShapeViewFactory {
// 2
var size: CGSize { get set }
// 3
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView)
}
Here’s how your new factory works:
- Define
ShapeViewFactory
as a Swift protocol. There’s no reason for it to be a class or struct since it only describes an interface and has no functionality itself. - Each factory should have a size that defines the bounding box of the shapes it creates. This is essential to layout code using the factory-produced views.
- Define the method that produces shape views. This is the “meat” of the factory. It takes a tuple of two Shape objects and returns a tuple of two ShapeView objects. This essentially manufactures views from its raw materials — the models.
Add the following code to end of ShapeViewFactory.swift:
class SquareShapeViewFactory: ShapeViewFactory {
var size: CGSize
// 1
init(size: CGSize) {
self.size = size
}
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
// 2
let squareShape1 = shapes.0 as! SquareShape
let shapeView1 =
SquareShapeView(frame: CGRect(x: 0,
y: 0,
width: squareShape1.sideLength * size.width,
height: squareShape1.sideLength * size.height))
shapeView1.shape = squareShape1
// 3
let squareShape2 = shapes.1 as! SquareShape
let shapeView2 =
SquareShapeView(frame: CGRect(x: 0,
y: 0,
width: squareShape2.sideLength * size.width,
height: squareShape2.sideLength * size.height))
shapeView2.shape = squareShape2
// 4
return (shapeView1, shapeView2)
}
}
Your SquareShapeViewFactory
produces SquareShapeView
instances as follows:
- Initialize the factory to use a consistent maximum size.
- Construct the first shape view from the first passed shape.
- Construct the second shape view from the second passed shape.
- Return a tuple containing the two created shape views.
Finally, it’s time to put SquareShapeViewFactory
to use. Open GameViewController.swift, and replace its contents with the following:
import UIKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1 ***** ADDITION
shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
beginNextTurn()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
private func beginNextTurn() {
let shape1 = SquareShape()
shape1.sideLength = Utils.randomBetweenLower(0.3, andUpper: 0.8)
let shape2 = SquareShape()
shape2.sideLength = Utils.randomBetweenLower(0.3, andUpper: 0.8)
// 2 ***** ADDITION
let shapeViews = shapeViewFactory.makeShapeViewsForShapes((shape1, shape2))
shapeViews.0.tapHandler = {
tappedView in
self.gameView.score += shape1.sideLength >= shape2.sideLength ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = {
tappedView in
self.gameView.score += shape2.sideLength >= shape1.sideLength ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(shapeViews)
}
private var gameView: GameView { return view as! GameView }
// 3 ***** ADDITION
private var shapeViewFactory: ShapeViewFactory!
}
There are three new lines of code:
- Initialize and store a
SquareShapeViewFactory
. - Use this new factory to create your shape views.
- Store your new shape view factory as an instance property.
The key benefits are in section two, where you replaced six lines of code with one. Better yet, you moved the complex shape view creation code out of GameViewController
to make the class smaller and easier to follow.
It’s helpful to move view creation code out of your view controller since GameViewController
acts as a view controller and coordinates between model and view.
Build and run, and then you should see something like the following:
Nothing about your game’s visuals changed, but you did simplify your code.
If you were to replace SquareShapeView
with SomeOtherShapeView
, then the benefits of the SquareShapeViewFactory
would shine. Specifically, you wouldn’t need to alter GameViewController
, and you could isolate all the changes to SquareShapeViewFactory
.
Now that you’ve simplified the creation of shape views, you’re going to simplify the creation of shapes. Create a new Swift file like before, called ShapeFactory.swift, and paste in the following code:
import Foundation
import UIKit
// 1
protocol ShapeFactory {
func createShapes() -> (Shape, Shape)
}
class SquareShapeFactory: ShapeFactory {
// 2
var minProportion: CGFloat
var maxProportion: CGFloat
init(minProportion: CGFloat, maxProportion: CGFloat) {
self.minProportion = minProportion
self.maxProportion = maxProportion
}
func createShapes() -> (Shape, Shape) {
// 3
let shape1 = SquareShape()
shape1.sideLength = Utils.randomBetweenLower(minProportion, andUpper: maxProportion)
// 4
let shape2 = SquareShape()
shape2.sideLength = Utils.randomBetweenLower(minProportion, andUpper: maxProportion)
// 5
return (shape1, shape2)
}
}
Your new ShapeFactory
produces shapes as follows:
- Again, you’ve declared the
ShapeFactory
as a protocol to build in maximum flexibility, just like you did forShapeViewFactory
. - You want your shape factory to produce shapes that have dimensions in unit terms, for instance, in a range like
[0, 1]
— so you store this range. - Create the first square shape with random dimensions.
- Create the second square shape with random dimensions.
- Return the pair of square shapes as a tuple.
Now open GameViewController.swift and insert the following line at the bottom just before the closing curly brace:
private var shapeFactory: ShapeFactory!
Then insert the following line near the bottom of viewDidLoad
, just above the invocation of beginNextTurn
:
shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
Finally, replace beginNextTurn
with this code:
private func beginNextTurn() {
// 1
let shapes = shapeFactory.createShapes()
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes)
shapeViews.0.tapHandler = {
tappedView in
// 2
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
// 3
self.gameView.score += square1.sideLength >= square2.sideLength ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = {
tappedView in
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(shapeViews)
}
Section by section, here’s what that does.
- Use your new shape factory to create a tuple of shapes.
- Extract the shapes from the tuple…
- …so that you can compare them here.
Once again, using the Abstract Factory design pattern simplified your code by moving shape generation out of GameViewController
.