Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

14. Prototype Pattern
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The prototype pattern is a creational pattern that allows an object to copy itself. It involves two types:

  1. A copying protocol that declares copy methods.

  2. A prototype class that conforms to the copying protocol.

There are actually two different types of copies: shallow and deep.

A shallow copy creates a new object instance, but doesn’t copy its properties. Any properties that are reference types still point to the same original objects. For example, whenever you copy a Swift Array, which is a struct and thereby happens automatically on assignment, a new array instance is created but its elements aren’t duplicated.

A deep copy creates a new object instance and duplicates each property as well. For example, if you deep copy an Array, each of its elements are copied too. Swift doesn’t provide a deep copy method on Array by default, so you’ll create one in this chapter!

When should you use it?

Use this pattern to enable an object to copy itself.

For example, Foundation defines the NSCopying protocol. However, this protocol was designed for Objective-C, and unfortunately, it doesn’t work that well in Swift. You can still use it, but you’ll wind up writing more boilerplate code yourself.

Instead, you’ll implement your own Copying protocol in this chapter. You’ll learn about the prototype pattern in depth this way, and your resulting implementation will be more Swifty too!

Playground example

Open IntermediateDesignPatterns.xcworkspace in the Starter directory, and then open the Prototype page.

public protocol Copying: class {
  // 1
  init(_ prototype: Self)
}

extension Copying {
  // 2
  public func copy() -> Self {
    return type(of: self).init(self)
  }
}
// 1
public class Monster: Copying {

  public var health: Int
  public var level: Int

  public init(health: Int, level: Int) {
    self.health = health
    self.level = level
  }

  // 2
  public required convenience init(_ monster: Monster) {
    self.init(health: monster.health, level: monster.level)
  }
}
// 1
public class EyeballMonster: Monster {

  public var redness = 0

  // 2
  public init(health: Int, level: Int, redness: Int) {
    self.redness = redness
    super.init(health: health, level: level)
  }

  // 3
  public required convenience init(_ prototype: Monster) {
    let eyeballMonster = prototype as! EyeballMonster
    self.init(health: eyeballMonster.health,
              level: eyeballMonster.level,
              redness: eyeballMonster.redness)
  }
}
let monster = Monster(health: 700, level: 37)
let monster2 = monster.copy()
print("Watch out! That monster's level is \(monster2.level)!")
Watch out! That monster's level is 37!
let eyeball = EyeballMonster(
  health: 3002,
  level: 60,
  redness: 999)
let eyeball2 = eyeball.copy()
print("Eww! Its eyeball redness is \(eyeball2.redness)!")
Eww! Its eyeball redness is 999!
let eyeballMonster3 = EyeballMonster(monster)
@available(*, unavailable, message: "Call copy() instead")
error: 'init' is unavailable: Call copy() instead

What should you be careful about?

As shown in the playground example, by default it’s possible to pass a superclass instance to a subclass’s copy initializer. This may not be a problem if a subclass can be fully initialized from a superclass instance. However, if the subclass adds any new properties, it may not be possible to initialize it from a superclass instance.

Tutorial project

Over the next few chapters, you’ll complete an app called MirrorPad. This is a drawing app that allows users to create animated mirror-image drawings.

// 1
public protocol Copying {
  init(_ prototype: Self)
}

extension Copying {
  public func copy() -> Self {
    return type(of: self).init(self)
  }
}

// 2
extension Array where Element: Copying {
  public func deepCopy() -> [Element] {
    return map { $0.copy() }
  }
}
public class LineShape: CAShapeLayer, Copying {
public override convenience init(layer: Any) {
  let lineShape = layer as! LineShape
  self.init(lineShape)
}

public required init(_ prototype: LineShape) {
  bezierPath = prototype.bezierPath.copy() as! UIBezierPath
  super.init(layer: prototype)

  fillColor = nil
  lineWidth = prototype.lineWidth
  path = bezierPath.cgPath
  strokeColor = prototype.strokeColor
}
public func copyLines(from source: DrawView) {
  layer.sublayers?.removeAll()
  lines = source.lines.deepCopy()
  lines.forEach { layer.addSublayer($0) }
}
mirrorDrawViews.forEach { $0.copyLines(from: inputDrawView) }
mirrorDrawViews.forEach { $0.animate() }

Key points

You learned about the prototype pattern in this chapter. Here are its key points:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now