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: Servant
At this point you can almost add a second shape, for example, a circle. Your only hard-coded dependence on squares is in the score calculation in beginNextTurn
in code like the following:
shapeViews.1.tapHandler = {
tappedView in
// 1
let square1 = shapes.0 as! SquareShape, square2 = shapes.1 as! SquareShape
// 2
self.gameView.score += square2.sideLength >= square1.sideLength ? 1 : -1
self.beginNextTurn()
}
Here you cast the shapes to SquareShape
so that you can access their sideLength
. Circles don’t have a sideLength
, instead they have a diameter
.
The solution is to use the Servant design pattern, which provides a behavior like score calculation to a group of classes like shapes, via a common interface. In your case, the score calculation will be the servant, the shapes will be the serviced classes, and an area
property plays the role of the common interface.
Open Shape.swift and add the following line to the bottom of the Shape
class:
var area: CGFloat { return 0 }
Then add the following line to the bottom of the SquareShape
class:
override var area: CGFloat { return sideLength * sideLength }
You can see where this is going — you can calculate which shape is larger based on its area.
Open GameViewController.swift and replace beginNextTurn
with the following:
private func beginNextTurn() {
let shapes = shapeFactory.createShapes()
let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes)
shapeViews.0.tapHandler = {
tappedView in
// 1
self.gameView.score += shapes.0.area >= shapes.1.area ? 1 : -1
self.beginNextTurn()
}
shapeViews.1.tapHandler = {
tappedView in
// 2
self.gameView.score += shapes.1.area >= shapes.0.area ? 1 : -1
self.beginNextTurn()
}
gameView.addShapeViews(shapeViews)
}
- Determines the larger shape based on the shape area.
- Also determines the larger shape based on the shape area.
Build and run, and you should see something like the following — the game looks the same, but the code is now more flexible.
Congratulations, you’ve completely removed dependencies on squares from your game logic. If you were to create and use some circle factories, your game would become more…well-rounded. :]
Leveraging Abstract Factory for Gameplay Versatility
“Don’t be a square!” can be an insult in real life, and your game feels like it’s been boxed in to one shape — it aspires to smoother lines and more aerodynamic shapes
You need to introduce some smooth “circley goodness.” Open Shape.swift, and then add the following code at the bottom of the file:
class CircleShape: Shape {
var diameter: CGFloat!
override var area: CGFloat { return CGFloat(M_PI) * diameter * diameter / 4.0 }
}
Your circle only needs to know the diameter
from which it can compute its area, and thus support the Servant pattern.
Next, build CircleShape
objects by adding a CircleShapeFactory
. Open ShapeFactory.swift, and add the following code at the bottom of the file:
class CircleShapeFactory: ShapeFactory {
var minProportion: CGFloat
var maxProportion: CGFloat
init(minProportion: CGFloat, maxProportion: CGFloat) {
self.minProportion = minProportion
self.maxProportion = maxProportion
}
func createShapes() -> (Shape, Shape) {
// 1
let shape1 = CircleShape()
shape1.diameter = Utils.randomBetweenLower(minProportion, andUpper: maxProportion)
// 2
let shape2 = CircleShape()
shape2.diameter = Utils.randomBetweenLower(minProportion, andUpper: maxProportion)
return (shape1, shape2)
}
}
This code follows a familiar pattern: Section 1 and Section 2 create a CircleShape
and assign it a random diameter
.
You need to solve another problem, and doing so might just prevent a messy Geometry Revolution. See, what you have right now is “Geometry Without Representation,” and you know how wound up shapes can get when they feel underrepresented. (haha!)
It’s easy to please your constituents; all you need to is represent your new CircleShape
objects on the screen with a CircleShapeView
. :]
Open ShapeView.swift
and add the following at the bottom of the file:
class CircleShapeView: ShapeView {
override init(frame: CGRect) {
super.init(frame: frame)
// 1
self.opaque = false
// 2
self.contentMode = UIViewContentMode.Redraw
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
if showFill {
fillColor.setFill()
// 3
let fillPath = UIBezierPath(ovalInRect: self.bounds)
fillPath.fill()
}
if showOutline {
outlineColor.setStroke()
// 4
let outlinePath = UIBezierPath(ovalInRect: CGRect(
x: halfLineWidth,
y: halfLineWidth,
width: self.bounds.size.width - 2 * halfLineWidth,
height: self.bounds.size.height - 2 * halfLineWidth))
outlinePath.lineWidth = 2.0 * halfLineWidth
outlinePath.stroke()
}
}
}
Explanations of the above that take each section in turn:
- Since a circle cannot fill the rectangular bounds of its view, you need to tell UIKit that the view is not opaque, meaning content behind it may poke through. If you miss this, then the circles will have an ugly black background.
- Because the view is not opaque, you should redraw the view when its bounds change.
- Draw a circle filled with the
fillColor
. In a moment, you’ll createCircleShapeViewFactory
, which will ensurethatCircleView
has equal width and height so the shape will be a circle and not an ellipse. - Stroke the outline border of the circle and inset to account for line width.
Now you’ll create CircleShapeView
objects in a CircleShapeViewFactory
.
Open ShapeViewFactory.swift and add the following code at the bottom of the file:
class CircleShapeViewFactory: ShapeViewFactory {
var size: CGSize
init(size: CGSize) {
self.size = size
}
func makeShapeViewsForShapes(shapes: (Shape, Shape)) -> (ShapeView, ShapeView) {
let circleShape1 = shapes.0 as! CircleShape
// 1
let shapeView1 = CircleShapeView(frame: CGRect(
x: 0,
y: 0,
width: circleShape1.diameter * size.width,
height: circleShape1.diameter * size.height))
shapeView1.shape = circleShape1
let circleShape2 = shapes.1 as! CircleShape
// 2
let shapeView2 = CircleShapeView(frame: CGRect(
x: 0,
y: 0,
width: circleShape2.diameter * size.width,
height: circleShape2.diameter * size.height))
shapeView2.shape = circleShape2
return (shapeView1, shapeView2)
}
}
This is the factory that will create circles instead of squares. Section 1 and Section 2 are creating CircleShapeView
instances by using the passed in shapes. Notice how your code is makes sure the circles have equal width and height so they render as perfect circles and not ellipses.
Finally, open GameViewController.swift and replace the lines in viewDidLoad
that assign the shape and view factories with the following:
shapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)
Now build and run and you should see something like the following screenshot.
Lookee there. You made circles!
Notice how you were able to add a new shape without much impact on your game’s logic in GameViewController
? The Abstract Factory and Servant design patterns made this possible.