• Intermediate Design Patterns in Swift
  • Raywenderlich.com
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: iWeslie
  • Proofread by: Swants, Kirinzer

Design patterns are very useful for maintaining code and improving readability, and in this tutorial you’ll learn some of the design patterns found in Swift.

What’s New: This tutorial has been updated by the translator for iOS 12, Xcode 10, and Swift 4.2.

Tutorial: Never understood design patterns? Check out the introductory design patterns tutorial to read the basics.

In this tutorial, you’ll learn how to use Swift’s design patterns to reconstruct a game called Tap the Larger Shape.

Understanding design patterns is critical to writing maintainable and bug-free applications, and knowing when to use which design patterns is a skill that can only be learned by practice. This tutorial couldn’t be better!

But what exactly are design patterns? This is a formal, documented solution to a common problem. For example, consider traversing a collection where you use the iterator design pattern:

var collection = ...

// The for loop uses the iterator design pattern
for item in collection {
    print("Item is: \(item)")}Copy the code

The value of the iterator design pattern is that it abstracts out the actual underlying mechanism for accessing each item in the collection. Whether a Collection is an array, a dictionary, or some other type, your code can access each of them the same way.

Not only that, but design patterns are part of the developer’s culture, so another developer maintaining or extending code might understand iterator design patterns, which are languages used to reason out software architecture.

There are a lot of design patterns that pop up frequently in iOS programming. MVC, for example, appears in almost every application. Proxies are a powerful and often underutilized pattern, like the tableView you’ve used before.

If you are not familiar with the concept of design patterns, this article may not be suitable for you right now, so let’s start by looking at iOS design patterns using Swift.

An introduction to

Tap the Larger Shape Tap the Larger Shape is a fun but simple game where you see a pair of similar shapes and Tap the Larger of the two. If you click on the larger shape, you get a point, if you click on the larger shape, you lose a point.

It looks like you’ve just squirted out some random squares, circles and triangles, but kids will pay! :]

Download the starter project and open it in Xcode.

Note: You need Xcode 10 and Swift 4.2 and above for maximum compatibility and stability.

This introductory project includes the complete game, where you will refactor the project and use some design patterns to make your game easier to maintain and more fun.

Using the iPhone 8 simulator, compile and run the project, and click on a few random graphics to learn the rules of the game. You should see something like this:

You can score points by clicking on larger shapes.

Clicking on a smaller image will take points away.

Understand the game

Before diving into the details of design patterns, let’s take a look at the games being written so far. Open Shape.swift and take a look and find the following code. You don’t need to change anything, just look:

import UIKit

class Shape {}class SquareShape: Shape {
	var sideLength: CGFloat!
}
Copy the code

Shape is the basic model for clickable graphics in games. A specific subclass, SquareShape, represents a square: a polygon with four equally long sides.

Next open shapeView.swift and look at the ShapeView code:

import UIKit

class ShapeView: UIView {
    var shape: Shape!

    / / 1
    var showFill: Bool = true {
        didSet {
            setNeedsDisplay()
        }
    }
    var fillColor: UIColor = UIColor.orange {
        didSet {
            setNeedsDisplay()
        }
    }

    / / 2
    var showOutline: Bool = true {
        didSet {
            setNeedsDisplay()
        }
    }
    var outlineColor: UIColor = UIColor.gray {
        didSet {
            setNeedsDisplay()
        }
    }

    / / 3
    var tapHandler: ((ShapeView) - > ())?override init(frame: CGRect) {
        super.init(frame: frame)

        / / 4
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        addGestureRecognizer(tapRecognizer)
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")}@objc func handleTap(a) {
        / / 5tapHandler? (self)}let halfLineWidth: CGFloat = 3.0
}
Copy the code

ShapeView is a view that renders a generic Shape model. Here’s a line-by-line parsing of the code:

  1. Specifies whether and what color the application uses to fill the graph, which is the color inside the graph.

  2. Specifies whether and what color the application uses to stroke the graph. This is the color of the graph border.

  3. A closure that handles click events (such as update scores). If you’re not familiar with Swift closures, you can look at them in Swift closures, but keep in mind that they’re similar to blocks in Objective-C.

  4. Set up a Tap Gesture recognizer, which calls handleTap when the player taps the View.

  5. TapHandler is called when a click gesture is detected.

Now scroll down and view the SquareShapeView:

class SquareShapeView: ShapeView {
	override func draw(_ rect: CGRect) {
		super.draw(rect)

        / / 1
		if showFill {
			fillColor.setFill()
			let fillPath = UIBezierPath(rect: bounds)
			fillPath.fill()
		}

        / / 2
		if showOutline {
			outlineColor.setStroke()

            / / 3
			let outlinePath = UIBezierPath(rect: CGRect(x: halfLineWidth, y: halfLineWidth, width: bounds.size.width - 2 * halfLineWidth, height: bounds.size.height - 2 * halfLineWidth))
			outlinePath.lineWidth = 2.0 * halfLineWidth
			outlinePath.stroke()
		}
	}
}
Copy the code

Here’s how the SquareShapeView draws:

  1. If configured to show a fill, the view is filled with the fill color.

  2. If configured to display Outlines, stroke the view with the outline color.

  3. Since iOS draws the line around position, we need to subtract the halfLineWidth from the View bounds when tracing the path.

Great! Now that you know how the graphics in this game are drawn, open up GameViewController.swift and see the logic:

import UIKit

class GameViewController: UIViewController {

	override func viewDidLoad(a) {
		super.viewDidLoad()
        / / 1
		beginNextTurn()
	}

	override var prefersStatusBarHidden: Bool {
		return true
	}

	private func beginNextTurn(a) {
        / / 2
		let shape1 = SquareShape()
		shape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
		let shape2 = SquareShape()
		shape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)

        / / 3
		let availSize = gameView.sizeAvailableForShapes()

        / / 4
		let shapeView1: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape1.sideLength, height: availSize.height * shape1.sideLength))
		shapeView1.shape = shape1
		let shapeView2: ShapeView = SquareShapeView(frame: CGRect(x: 0, y: 0, width: availSize.width * shape2.sideLength, height: availSize.height * shape2.sideLength))
		shapeView2.shape = shape2

        / / 5
		let shapeViews = (shapeView1, shapeView2)

        / / 6
		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()
		}

        / / 7
		gameView.addShapeViews(newShapeViews: shapeViews)
	}

	private var gameView: GameView { return view as! GameView}}Copy the code

Here’s how game logic works:

  1. Start a new game when the GameView loads.

  2. Draw a square with side lengths in the range [0.3, 0.8]. The drawn graph can also be scaled at any screen size.

  3. GameView determines which graphics size fits the current screen.

  4. Create a SquareShapeView for each shape and resize the shape by multiplying the sideLength ratio of the graph by the corresponding availSize of the current screen.

  5. Store shapes in tuples for easy manipulation.

  6. Click events are set on each Shape View and scores are calculated based on whether the player clicks on the larger view.

  7. Add the shape to the GameView so that the layout can be displayed.

That’s the whole logic of the game. Isn’t that easy? :]

Why use design patterns?

You might be asking yourself, “HMM, so why do I need design patterns when I have a work game?” So what if you want to support shapes other than squares?

You could have added code to beginNextTurn to create a second shape, but when you add a third, fourth, or even fifth shape, the code becomes unmanageable.

What if you wanted the player to be able to choose someone else’s shape?

If you put all your code in a GameViewController, you’ll end up with unmanageable, highly coupled code with hard-coded dependencies.

Here’s the answer to your question: Design patterns help decouple your code into discrete units.

Before I take the next step, I confess that I’ve slipped into design mode.

Now, with regard to design patterns, each of the following sections describes a different design pattern. Let’s get started!

Abstract Factory pattern

The GameViewController is tightly coupled to the SquareShapeView, which makes it impossible to use a different view to represent the square later or to introduce a second shape.

Your first task is to simplify and decouple your GameViewController using the abstract factory design pattern. You’ll use this pattern in code that builds an API for constructing a set of related objects, such as the Shape View you’ll use temporarily, without hard-coding a specific class.

Create a new Swift file, name it shapeViewFactory.swift and save it, then add the following code:

import UIKit

/ / 1
protocol ShapeViewFactory {
    / / 2
    var size: CGSize { get set }
    / / 3
    func makeShapeViewsForShapes(shapes: (Shape, Shape)) - > (ShapeView.ShapeView)}Copy the code

Here’s how your new factory will work:

  1. Defining ShapeViewFactory as the Swift protocol, it has no reason to be a class or structure, because it describes only an interface and has no functionality of its own.

  2. Each factory should have a size that defines the boundaries for creating shapes, which is critical to using the factory-generated view layout code.

  3. Defines a method to generate a shape view. This is the factory meat, which takes two tuples of Shape objects and returns two tuples of ShapeView objects. This is basically making the View from its raw material, the model.

Add the following code to the 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)
    }
}
Copy the code

Your SquareShapeViewFactory builds a SquareShapeView instance like this:

  1. Use a consistent maximum size to initialize the chemical plant.

  2. Construct the first Shape View from the first passed shape.

  3. Construct the second Shape View from the second passed shape.

  4. Returns a tuple containing two newly created Shape Views.

Finally, it’s time to use the SquareShapeViewFactory. Open up gameViewController.swift and replace it all with the following:

import UIKit

class GameViewController: UIViewController {

	override func viewDidLoad(a) {
		super.viewDidLoad()
        // 1 ***** attach
        shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())

		beginNextTurn()
	}

	override var prefersStatusBarHidden: Bool {
		return true
	}

	private func beginNextTurn(a) {
		let shape1 = SquareShape()
		shape1.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)
		let shape2 = SquareShape()
		shape2.sideLength = Utils.randomBetweenLower(lower: 0.3, andUpper: 0.8)

        // 2 ***** attach
        let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: (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(newShapeViews: shapeViews)
	}

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

    // 3 ***** attach
    private var shapeViewFactory: ShapeViewFactory!
}
Copy the code

Here are three new lines of code:

  1. Initialize and store a SquareShapeViewFactory.

  2. Use this new factory to create your Shape View.

  3. Store the new Shape View factory as an instance property.

The main benefit is in the second part, where you replace six lines of code with one. Even better, you move the complex shape View creation code out of the GameViewController to make the class smaller and easier to understand.

It’s helpful to move the view creation code out of the Controller, because the GameViewController acts as a controller to mediate between the Model and the View.

Compile and run, and you should see something like the following:

The visuals of your game didn’t change anything, but you did simplify the code.

If you replace SquareShapeView with SomeOtherShapeView, the benefits of the SquareShapeViewFactory will shine through. Specifically, you don’t need to change the GameViewController, you can separate all changes to the SquareShapeViewFactory.

Now that you’ve simplified shape View creation, you can also simplify shape creation. Create a new Swift file as before, call it shapeFactory.swift, and paste in the following code:

import UIKit

/ / 1
protocol ShapeFactory {
    func createShapes(a)- > (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(a)- > (Shape.Shape) {
        / / 3
        let shape1 = SquareShape()
        shape1.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)

        / / 4
        let shape2 = SquareShape()
        shape2.sideLength = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)

        / / 5
        return (shape1, shape2)
    }
}
Copy the code

The steps for your new ShapeFactory shape production are as follows:

  1. Again, just like you did with ShapeViewFactory, declare the ShapeFactory as a protocol for maximum flexibility.

  2. You want your Shape factory to generate shapes with unit sizes, for example, in the range [0, 1], so you store this range.

  3. Creates the first square with a random size.

  4. Creates a second square with a random size.

  5. Return the pair of square shapes as a tuple.

Now open up gameViewController.swift and insert the following code before closing the braces at the bottom:

private var shapeFactory: ShapeFactory!
Copy the code

Then insert the following code above the call to beginNextTurn at the bottom of viewDidLoad:

shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
Copy the code

Finally replace beginNextTurn with the following code:

private func beginNextTurn(a) {
    / / 1
    let shapes = shapeFactory.createShapes()

    let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: 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(newShapeViews: shapeViews)
}
Copy the code

Here is a breakdown of the above code:

  1. Create a shape tuple using the new Shape factory.

  2. Extract shapes from tuples.

  3. So you can compare them here.

Once again, the abstract factory design pattern is used to simplify the code by moving the part that creates the shape away from the GameViewController.

Employment pattern

Now you can even add a second shape, such as a circle. Your only hard dependence on the square is the score calculation in beginNextTurn below:

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()
}
Copy the code

Here you convert shapes to SquareShape so that you can access their sideLength, circles don’t have a sideLength, they have a “diameter”.

The solution is to use the hired hand design pattern, which provides methods such as fractional calculation for a set of classes, such as shape classes, through a common interface. In your current situation, the score calculation is the hired hand, the shape class is the service object, and the Area property acts as the public interface.

Open Shape.swift and add the following code to the bottom of the Shape class:

var area: CGFloat { return 0 }
Copy the code

Then add the following code at the bottom of the SquareShape class:

override var area: CGFloat { return sideLength * sideLength }
Copy the code

Now you can tell which shape is bigger by its area.

Open gameViewController.swift and replace beginNextTurn with the following:

private func beginNextTurn(a) {
    let shapes = shapeFactory.createShapes()

    let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: 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(newShapeViews: shapeViews)
}
Copy the code
  1. Determine the larger shape based on the shape region.

  2. Again, the larger shape is based on the shape region.

Compile and run, and you should see something like the following, although the game looks the same, the code is now more flexible.

Congratulations, you’ve completely removed the dependency on squares from your game logic, and your game will be much more polished if you build and use some circular factories.

Use abstract factories for game versatility

“Don’t be a square! What might be an insult in real life, your game feels like it’s trapped in a shape, and it craves smoother lines and more aerodynamic shapes.

You need to introduce some smooth “nice circles”, now open Shape.swift and add the following code at the bottom of the file:

class CircleShape: Shape {
    var diameter: CGFloat!
    override var area: CGFloat { return CGFloat.pi * diameter * diameter / 4.0}}Copy the code

Your circle only needs to know that it can calculate its own area “diameter” to support the hired hand mode.

Next build the CircleShape object 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(a)- > (Shape.Shape) {
		/ / 1
		let shape1 = CircleShape()
		shape1.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)

		/ / 2
		let shape2 = CircleShape()
		shape2.diameter = Utils.randomBetweenLower(lower: minProportion, andUpper: maxProportion)

		return (shape1, shape2)
	}
}
Copy the code

This code follows a familiar pattern: Parts 1 and 2 create a CircleShape and assign it a random diameter.

You need to solve another problem, and doing so might prevent a messy geometry revolution. See, what you have now is “unrepresentative geometry”, and you know how clean shapes can get when they’re not enough!

Pleasing your players is easy, all you need is to use CircleShapeView to draw your new CircleShape object on the screen. :]

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.isOpaque = false
		/ / 2
		self.contentMode = UIView.ContentMode.redraw
	}

	required init(coder aDecoder: NSCoder) {
		fatalError("init(coder:) has not been implemented")}override func draw(_ rect: CGRect) {
		super.draw(rect)

		if showFill {
			fillColor.setFill()
			/ / 3
			let fillPath = UIBezierPath(ovalIn: self.bounds)
			fillPath.fill()
		}

		if showOutline {
			outlineColor.setStroke()
			/ / 4
			let outlinePath = UIBezierPath(ovalIn: 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()
		}
	}
}
Copy the code

The above contents are explained in the following parts:

  1. Since the circle can’t fill its view bounds, you need to tell UIKit that the view is transparent, which means you can see through it. If you are not aware of this, the circle will have an ugly black background.

  2. Because the view is transparent, it should be redrawn when the bounds change.

  3. Draw a circle filled with fillColor. Later, you will create a CircleShapeViewFactory, which will ensure that the CircleView is of equal width and height, so that the drawn shape will be round rather than oval.

  4. Stroke the circle with lineWidth.

Now you will create a CircleShapeView object in the 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)
	}
}
Copy the code

This is the factory that will create circles instead of squares. Parts 1 and 2 create CircleShapeView instances using the incoming shapes. Notice how your code ensures that the circles have the same width and height, so they appear as perfect circles rather than ellipses.

Finally, open gameViewController.swift and replace the corresponding two lines in viewDidLoad, assigning shapes and view factories with the following:

shapeViewFactory = CircleShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = CircleShapeFactory(minProportion: 0.3, maxProportion: 0.8)
Copy the code

Now compile and run the project, you should see something like the screenshot below.

Notice how you can add new shapes to the GameViewController without too much impact on the game logic, made possible by abstract factory and hired hands design patterns.

Builder model

Now it’s time to look at the third design pattern: the builder.

Suppose you want to change the appearance of ShapeView instances – such as whether they should be displayed, and what color to fill and stroke. The Builder design pattern makes it easier and more flexible to configure such objects.

A method to solve the problem of this configuration is to add a variety of constructors, can use such as CircleShapeView. RedFilledCircleWithBlueOutline () class initialization method of convenience, can also be added with various parameters and the default initialization method.

Unfortunately, it’s not an extensible technique because you need to write new methods or initializers for each combination.

The builder solved this problem very elegantly by creating a single-purpose class to configure the already-initialized object. If you are going to have the builder build the red circle and then the blue circle, you can do this without changing the CircleShapeView.

Create a new file shapeViewBuilder.swift and add the following code:

import UIKit

class ShapeViewBuilder {
	/ / 1
	var showFill  = true
	var fillColor = UIColor.orange

	/ / 2
	var showOutline  = true
	var outlineColor = UIColor.gray

	/ / 3
	init(shapeViewFactory: ShapeViewFactory) {
		self.shapeViewFactory = shapeViewFactory
	}

	/ / 4
	func buildShapeViewsForShapes(shapes: (Shape, Shape)) - > (ShapeView.ShapeView) {
		let shapeViews = shapeViewFactory.makeShapeViewsForShapes(shapes: shapes)
		configureShapeView(shapeView: shapeViews.0)
		configureShapeView(shapeView: 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
}
Copy the code

Here’s how your new ShapeViewBuilder works:

  1. Store the fill properties of the ShapeView configuration.

  2. Store the stroke properties of the ShapeView configuration.

  3. Initialize the builder to hold the ShapeViewFactory to construct the View. This means that the builder does not need to know whether it is building a SquareShapeView or a CircleShapeView or some other shape of view.

  4. This is the public API that creates and initializes a pair of ShapeViews when you have a pair of Shapes.

  5. Configure the ShapeView based on the builder’s stored configuration.

Now deploy your new ShapeViewBuilder, open gameViewController.swift, and add the following code to the bottom of the class before the curly braces end:

private var shapeViewBuilder: ShapeViewBuilder!
Copy the code

Now add the following code above the beginNextTurn call in viewDidLoad to fill in the new property:

shapeViewBuilder = ShapeViewBuilder(shapeViewFactory: shapeViewFactory)
shapeViewBuilder.fillColor = UIColor.brown
shapeViewBuilder.outlineColor = UIColor.orange
Copy the code

Finally replace the shapeViews line in beginNextTurn with the following code:

let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)
Copy the code

Compile and run, and you should see the following:

To be honest, I think the fill color is ugly too, but don’t make fun of it, we’re not focusing on how good it looks.

Now to strengthen the builders. Again in GameViewController.swift, change the two lines corresponding to viewDidLoad to use square factories:

shapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
shapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
Copy the code

Compile and run, and you should see the following:

Notice how the Builder mode applies the new color scheme to the squares and circles. Without it you need to set the color separately in CircleShapeViewFactory and SquareShapeViewFactory.

In addition, changing to a different color scheme would involve a lot of code modification. You can also isolate color changes to a single class by limiting the ShapeView color configuration to a single ShapeViewBuilder.

Dependency injection mode

Each time you click on a shape, you play a turn, and the result of each turn can be a score or a minus.

Wouldn’t it help if your game could automatically track all turns, stats, and scores?

Create a new file called Turn. Swift and replace its contents with the following code:

class Turn {
    / / 1
    let shapes: [Shape]
    var matched: Bool?

    init(shapes: [Shape]) {
        self.shapes = shapes
    }

    / / 2
    func turnCompletedWithTappedShape(tappedShape: Shape) {
        let maxArea = shapes.reduce(0) {$0 > $1.area ? $0 : $1.area }
        matched = tappedShape.area >= maxArea
    }
}
Copy the code

Your new Turn class does the following:

  1. Stores the shapes the player sees each turn, and whether they clicked on the larger shapes.

  2. Note that the turn has ended after the player clicks on the shape.

To control the order in which sessions the player plays, create a new file called TurnController.swift and replace its contents with the following code:

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(a)- > (ShapeView.ShapeView) {
        let shapes = shapeFactory.createShapes()
        let shapeViews = shapeViewBuilder.buildShapeViewsForShapes(shapes: shapes)
        currentTurn = Turn(shapes: [shapeViews.0.shape, shapeViews.1.shape])
        return shapeViews
    }

    / / 4
    func endTurnWithTappedShape(tappedShape: Shape) -> Int{ currentTurn! .turnCompletedWithTappedShape(tappedShape: tappedShape) pastTurns.append(currentTurn!)letscoreIncrement = currentTurn! .matched! ?1 : -1

        return scoreIncrement
    }

    private let shapeFactory: ShapeFactory
    private var shapeViewBuilder: ShapeViewBuilder
}
Copy the code

Your TurnController works as follows:

  1. Stores current and past rounds.

  2. Receive a ShapeFactory and a ShapeViewBuilder.

  3. Use this factory and builder to create shapes and views for each new turn and record the current turn.

  4. The end of the turn is recorded after the player clicks on the shape, and the score is calculated based on the shape that the player clicks on that turn.

Now open up gameViewController.swift and add the following code above the bottom braces:

private var turnController: TurnController!
Copy the code

Scroll up to viewDidLoad and insert the following code before calling the beginNewTurn line:

turnController = TurnController(shapeFactory: shapeFactory, shapeViewBuilder: shapeViewBuilder)
Copy the code

Replace beginNextTurn with the following code:

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

    shapeViews.0.tapHandler = { tappedView in
        / / 2
        self.gameView.score += self.turnController.endTurnWithTappedShape(tappedShape: tappedView.shape)
        self.beginNextTurn()
    }

    / / 3
    shapeViews.1.tapHandler = shapeViews.0.tapHandler

    gameView.addShapeViews(newShapeViews: shapeViews)
}
Copy the code

Here’s how your new code works:

  1. Tell the TurnController to start a new turn and return a ShapeView tuple for the turn.

  2. When the player clicks on the ShapeView, the controller is notified that the turn is over and the score is calculated. Notice how TurnController abstracts away the scoring process and further simplifies the GameViewController.

  3. Because you remove explicit references to a particular shape, the second shape view can share the same tapHandler closure as the first shape view.

An example application of the dependency injection design pattern is that it passes its dependencies to the TurnController initializer, whose parameters are mainly shapes and factory dependencies to inject.

Since TurnController does not assume which type of factory to use, you are free to switch between different factories.

Not only does this make your game more flexible, it also makes automated testing easier. It allows you to pass arguments to the special TestShapeFactory and TestShapeViewFactory classes if you want. These could be special stubs or mocks that make testing easier, more reliable, and faster.

Build and run and check that it looks like this: Compile and run, you should see the following image:

The interface doesn’t seem to have changed much, but TurnController has opened up your code so it can use more sophisticated turn mechanics: score points based on turn and then selectively change shape between turns, and even adjust the difficulty of matches based on player performance.

The strategy pattern

I’m really happy because I was eating a piece of pie while writing this tutorial, and maybe that’s why we added circles to the game. :]

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

Speaking of pies, uh, Pi, how do you put those circles back into the game? Now your GameViewController can use circles or squares, but only one or the other. It doesn’t have to be strictly limited.

Next you will use strategy mode to manage shapes in the game.

The policy design pattern allows you to design algorithms based on what the program determines at runtime. In this case, the algorithm chooses what shape to present to the player.

You can design many different algorithms: one that randomly selects shapes, one that selects shapes to give the player a little challenge or help him win more, and so on! Policies define a set of algorithms by an abstract declaration of the behavior that each policy must implement, which makes the algorithms within the family interchangeable.

If you assume that you will implement the policy as a Swift Protocol, you are right!

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

/ / 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(shapes: 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(lower: 0.0, andUpper: 100.0) < 50.0 {
            return firstStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)
        } else {
            return secondStrategy.makeShapeViewsForNextTurnGivenPastTurns(pastTurns: pastTurns)
        }
    }
}
Copy the code

Here’s what your new TurnStrategy does:

  1. This is an abstract method defined in a protocol that takes an array of the last turn in the game and returns a shape view to show the next turn.

  2. Implement a basic strategy using ShapeFactory and ShapeViewBuilder that implements the existing behavior where the shape view comes from a single factory and builder as before. Note that you are using dependency injection again here, which means that this policy does not care which factory or builder it uses.

  3. A random strategy is implemented using one of the other two strategies. You’re using a combination here, so RandomTurnStrategy can behave like two potentially different strategies. But because it is a policy, any code that uses RandomTurnStrategy hides the combination.

  4. This is the heart of random strategy. It randomly selects either the first or the second strategy with a 50% probability.

Now you need to use your strategy. Open turnController.swift and replace it with the following:


class TurnController {
    var currentTurn: Turn?
    var pastTurns: [Turn] = [Turn] ()/ / 1
    init(turnStrategy: TurnStrategy) {
        self.turnStrategy = turnStrategy
    }

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

    func endTurnWithTappedShape(tappedShape: Shape) -> Int{ currentTurn! .turnCompletedWithTappedShape(tappedShape: tappedShape) pastTurns.append(currentTurn!)letscoreIncrement = currentTurn! .matched! ?1 : -1

        return scoreIncrement
    }

    private let turnStrategy: TurnStrategy
}
Copy the code

Here are the detailed steps:

  1. Receive the passed policy and store it in a TurnController instance.

  2. Use strategy to generate ShapeView objects so that the player can start a new turn.

Note: This will cause a syntax error in gameViewController.swift. But don’t worry, this is only temporary and you will fix the bug in the next step.

The final step in using the strategy design pattern is to adjust your GameViewController to use your TurnStrategy.

Open gameViewController.swift and replace it with the following:

import UIKit

class GameViewController: UIViewController {

    override func viewDidLoad(a) {
        super.viewDidLoad()

        / / 1
        let squareShapeViewFactory = SquareShapeViewFactory(size: gameView.sizeAvailableForShapes())
        let squareShapeFactory = SquareShapeFactory(minProportion: 0.3, maxProportion: 0.8)
        let squareShapeViewBuilder = shapeViewBuilderForFactory(shapeViewFactory: 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(shapeViewFactory: circleShapeViewFactory)
        let circleTurnStrategy = BasicTurnStrategy(shapeFactory: circleShapeFactory, shapeViewBuilder: circleShapeViewBuilder)

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

        / / 4
        turnController = TurnController(turnStrategy: randomTurnStrategy)

        beginNextTurn()
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }

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

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

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

        gameView.addShapeViews(newShapeViews: shapeViews)
    }

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

The detailed steps for your modified GameViewController to use TurnStrategy are as follows:

  1. Create a policy to create a square.

  2. Create a policy to create circles.

  3. Create a policy to randomly choose whether to use a square or circular strategy.

  4. Create a turn controller to use random strategy.

Compile and run, then play five or six rounds, and you should see something like the following.

Notice how your game alternates randomly between squares and circles. At this point you can easily add a third shape, such as a triangle or parallelogram, and your GameViewController can use it by switching strategies.

Chain of responsibility, command and iterator patterns

Consider the example at the beginning of this tutorial:

var collection = ...

// The for loop uses the iterator design pattern
for item in collection {
    print("Item is: \(item)")}Copy the code

What makes the for Item in Collection loop work? The answer is Swift’s SequenceType.

By means of… Using the iterator pattern in the in loop, you can iterate over any type that follows the SequenceType protocol.

The built-in collection types Array and Dictionary follow SequenceType, so unless you’re writing your own collection, you usually don’t need to think about SequenceType. Still, I was happy to learn about the model. :]

Another design pattern you often see used in conjunction with iterators is the command pattern, which captures the concept of invoking a specific behavior on a target when asked.

In this tutorial you will use commands to determine the winner of a round and calculate the score of the game.

Create a new file named Scorer. Swift and replace it with the following code:

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

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

Take a look at each step in turn:

  1. Define your command type and declare its behavior so that it receives a set of all past turns that you can iterate over with iterators.

  2. A concrete implementation of a Scorer that calculates points based on whether they win or not.

  3. Use iterators to iterate over past turns.

  4. A winning round is scored at +1 and a losing round at -1.

Now open turnController.swift and add the following code to the bottom of the class:

private var scorer: Scorer
Copy the code

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

self.scorer = MatchScorer(a)Copy the code

Finally, Replace the line in endTurnWithTappedShape that declares and sets scoreIncrement with the following Replace the declaration of scoreIncrement in endTurnWithTappedShape with:

let scoreIncrement = scorer.computeScoreIncrement(pastTurns.reversed())
Copy the code

Note that you will reverse pastTurns before calculating the score, because the order in which the score is calculated is the opposite of the order in which the turn takes place, and pastTurns stores the original turn, in other words we will append the latest turn at the end of the array.

Compiling and running the project, do you notice anything strange? I bet your score hasn’t changed for some reason.

You need to use the chain of responsibility model to change your score.

The chain of responsibility pattern captures the concept of scheduling multiple commands across a set of data. In this exercise, you will send different Scorer commands to calculate your player score in a variety of additional ways.

For example, not only do you add or subtract a point for a match, but you also get bonus points for winning consecutive matches. Chains of responsibility allow you to add the implementation of a second Scorer in a way that does not disrupt the existing Scorer.

Open Scorer. Swift and add the following code at the top of MatchScorer:

var nextScorer: Scorer? = nil
Copy the code

Then add at the end of the Scorer protocol:

var nextScorer: Scorer? { get set }
Copy the code

Now MatchScorer and all the other Scorer show that they implement the chain of responsibility pattern through the nextScorer attribute.

Replace the return statement with the following code in computeScoreIncrement:

return (scoreIncrement ?? 0) + (nextScorer? .computeScoreIncrement(pastTurnsReversed) ??0)
Copy the code

Now you can add another Scorer to the chain after the MatchScorer and its score is automatically added to the score calculated by the MatchScorer.

Note:?????? Operator is Swift’s merge null-value operator. Expands the optional value if it is non-nil, and returns?? And then another value. Actually A?? With a b! = nil ? a! B. This is a good shorthand, and we 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>(_ pastTurnsReversed: S) -> Int where S : Sequence.S.Element= =Turn {
        / / 1
        var streakLength = 0
        for turn in pastTurnsReversed {
            if turn.matched! {
                / / 2
                streakLength += 1
            } else {
                / / 3
                break}}/ / 4
        let streakBonus = streakLength >= 5 ? 10 : 0
        returnstreakBonus + (nextScorer? .computeScoreIncrement(pastTurnsReversed) ??0)}}Copy the code

Here’s how your beautiful new StreakScorer works:

  1. The number of consecutive victories.

  2. If the round is won, the number of consecutive times is increased by one.

  3. If the round is lost, the number of consecutive wins is cleared.

  4. Calculate the bonus for winning 5 or more games in a row and get 10 points!

To complete chain of responsibility mode, open TurnController.swift and add the following line to the end of the initializer init(turnStrategy:) :

self.scorer.nextScorer = StreakScorer(a)Copy the code

Good, now you’re using the chain of responsibility.

Compile and run, and after winning the first five rounds, you should see the screenshot below.

Notice how the score goes from 5 to 16, because the bonus points of 10 and the point of 1 in the sixth are counted for 5 games in a row.

What’s next?

Here is the final project for this tutorial.

You play a fun game called Tap the Larger Shape and use Design Mode to add more shapes and enhance the Shape. You also use Design Mode to calculate the score more accurately.

Most notably, even though the final project has more features, the code is actually simpler and easier to maintain than when you started.

Why not use these design patterns to further expand your game? Try the following ideas.

Add more shapes like triangles, parallelograms, stars, etc. Tip: Think back to how you added circles and follow a similar sequence of steps to add new shapes. If you come up with some really cool shapes, you can try them out for yourself!

Add an animated tip for score changes: use didSet on gameView.score.

Add controls to let the player choose the type of shape the game will use: Add three UIButtons or a UISegmentedControl with Square, Circle, and Mixed options to the GameView, which should forward any click events on the control to the observer via a closure. GameViewController can use these closures to adjust the TurnStrategy it uses.

Tip for leaving shape Settings as recoverable preferences: Store the shape types selected by the player in UserDefaults. Try using appearance patterns (detailed) to hide your choice of someone else’s persistence mechanism.

Tip: Use UserDefaults to persistently store the player’s choices. Create a ShapeViewBuilder that accepts persistent selections and adjusts the application’s UI accordingly. When the color scheme changes, can you use NotificationCenter to notify all relevant Views to update them accordingly?

A celebratory ring whenever a player wins and a sad ring whenever a player loses: Extend the observer mode used between GameView and GameViewController.

Tip: remove references to MatchScorer and StreakScorer from the initializer.

Thank you for completing this tutorial! You can share your questions, thoughts and ways to improve your game in the comments section.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.