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 3 of 6 of this article. Click here to view the first page.

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)
}
  1. Determines the larger shape based on the shape area.
  2. 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.

Screenshot6

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

ragecomic2

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:

  1. 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.
  2. Because the view is not opaque, you should redraw the view when its bounds change.
  3. Draw a circle filled with the fillColor. In a moment, you’ll create CircleShapeViewFactory, which will ensurethat CircleView has equal width and height so the shape will be a circle and not an ellipse.
  4. 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.

Screenshot7
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.