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 .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 6 of this article. Click here to view the first page.

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. :]
ragecomic1
[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:

  1. 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.
  2. 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.
  3. 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:

  1. Initialize the factory to use a consistent maximum size.
  2. Construct the first shape view from the first passed shape.
  3. Construct the second shape view from the second passed shape.
  4. 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:

  1. Initialize and store a SquareShapeViewFactory.
  2. Use this new factory to create your shape views.
  3. 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:

Screenshot4

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:

  1. Again, you’ve declared the ShapeFactory as a protocol to build in maximum flexibility, just like you did for ShapeViewFactory.
  2. 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.
  3. Create the first square shape with random dimensions.
  4. Create the second square shape with random dimensions.
  5. 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.

  1. Use your new shape factory to create a tuple of shapes.
  2. Extract the shapes from the tuple…
  3. …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.