14.
Prototype Pattern
Written by Joshua Greene
The prototype pattern is a creational pattern that allows an object to copy itself. It involves two types:
-
A copying protocol that declares copy methods.
-
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.
For the example, you’ll create a Copying protocol and a Monster class that conforms to that protocol. Add the following after Code Example:
public protocol Copying: class {
// 1
init(_ prototype: Self)
}
extension Copying {
// 2
public func copy() -> Self {
return type(of: self).init(self)
}
}
-
You first declare a required initializer,
init(_ prototype: Self). This is called a copy initializer as its purpose is to create a new class instance using an existing instance. -
You normally won’t call the copy initializer directly. Instead, you’ll simply call
copy()on a conformingCopyingclass instance that you want to copy.Since you declared the copy initializer within the protocol itself,
copy()is extremely simple. It determines the current type by callingtype(of: self), and it then calls the copy initializer, passing in theselfinstance. Thereby, even if you create a subclass of a type that conforms toCopying,copy()will function correctly.
Next, ddd the following code:
// 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)
}
}
Here’s what that code does:
-
This declares a simple
Monstertype, which conforms toCopyingand has properties forhealthandlevel. -
In order to satisfy
Copying, you must declareinit(_ prototype:)asrequired. However, you’re allowed to mark this asconvenienceand call another designated initializer, which is exactly what you do.
Next, add the following code:
// 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)
}
}
Taking the above code comment-by-comment:
-
In a real app, you’d likely have
Monstersubclasses as well, which would add additional properties and functionality. Here, you declare anEyeballMonster, which adds a terrifying new property,redness. Oooh, it’s so red and icky! Don’t touch that eyeball! -
Since you added a new property, you also need to set its value upon initialization. To do so, you create a new designated initializer:
init(health:level:redness:). -
Since you created a new initializer, you must also provide all other
requiredinitializers. Note that you need to implement this with the general type,Monster, and then cast it to anEyeballMonster. That’s because specializing toEyeballMonsterwould mean that it couldn’t take another subclass ofMonster, which would break the condition that this is overriding the required initializer fromMonster.
You’re now ready to try out these classes! Add the following:
let monster = Monster(health: 700, level: 37)
let monster2 = monster.copy()
print("Watch out! That monster's level is \(monster2.level)!")
Here, you create a new monster, create a copy named monster2 and then print monster2.level. You should see this output in the console:
Watch out! That monster's level is 37!
Enter the following next:
let eyeball = EyeballMonster(
health: 3002,
level: 60,
redness: 999)
let eyeball2 = eyeball.copy()
print("Eww! Its eyeball redness is \(eyeball2.redness)!")
You here prove that you can indeed create a copy of EyeBallMonster. You should see this output in the console:
Eww! Its eyeball redness is 999!
What happens if you try to create an EyeballMonster from a Monster ? Enter the following last:
let eyeballMonster3 = EyeballMonster(monster)
This compiles fine, but it causes a runtime exception. This is due to the forced cast you performed earlier, where you called prototype as! EyeballMonster.
Comment out this line so the playground can run again.
Ideally, you should not allow calls to init(_ monster:) on any subclasses of Monster. Instead, you should always call copy().
You can indicate this to other developers by marking the subclass method as “unavailable.” Add the following line right before the subclass’s init(_ monster:):
@available(*, unavailable, message: "Call copy() instead")
Then, uncomment the line for eyeballMonster3, and you’ll get this error message in the playground console:
error: 'init' is unavailable: Call copy() instead
Great, this prevents calling this method directly! Go ahead and comment out the line again so the playground can run.
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.
To mitigate this issue, you can mark the subclass copy initializer as “unavailable.” In response, the compiler will refuse to compile any direct calls to this method.
It’s still possible to call the method indirectly, like copy() does. However, this safeguard should be “good enough” for most use cases.
If this doesn’t prevent issues for your use case, you’ll need to consider how exactly you want to handle it. For example, you may print an error message to the console and crash, or you may handle it by providing default values instead.
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.
In the Starter directory, open MirrorPad\MirrorPad.xcodeproj in Xcode.
Build and run to try out the app. Draw into the top-left view by using your finger on a real device or mouse on the simulator.
Then press Animate, and your drawing will be re-drawn animated on screen. Super cool!
However, the app is supposed to copy and reflect the image into each of the other views. This currently isn’t implemented because the app doesn’t know how to copy anything! It’s your job to fix this.
Open DrawView.swift and check out this class. This is the heart of the application: it creates a new LineShape object when touchesBegan is called and adds points to LineShape when touchesMoved is called.
Next, open LineShape.swift and check out this class. This is a subclass of CAShapeLayer (see https://developer.apple.com/documentation/quartzcore/cashapelayer), which is used to create simple, light-weight shape layers from paths. If LineShape were copyable, you’d be able to duplicate each of them into the other DrawView instances on screen.
First, however, you actually need to define what “copyable” actually means!
Under the Protocols group in the File hierarchy, create a new Swift file named Copying.swift and replace its contents with the following:
// 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() }
}
}
-
You first declare a new
Copyingprotocol, which is exactly the same as the one in the playground example. -
You then create an extension on
Arraywhen itsElementconforms toCopying. Therein, you create a new method calleddeepCopy(), which usesmapto create a new array where each element is generated by callingcopy().
Return back to LineShape.swift, and replace the class declaration with the following:
public class LineShape: CAShapeLayer, Copying {
Then, replace init(layer: Any) with the following:
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
}
init(layer:) looks very familiar to init(_ prototype:). This method is used internally by Core Animation during layer animations. In order to actually conform to Copying, however, the method signature must exactly match init(_:). Thereby, you simply hand off init(layer:) to init(_:), and both Core Animation and Copying requirements are satisfied.
You also need a method to actually copy each LineShape onto the DrawView. Open DrawView.swift and add the following right before the ending class curly brace:
public func copyLines(from source: DrawView) {
layer.sublayers?.removeAll()
lines = source.lines.deepCopy()
lines.forEach { layer.addSublayer($0) }
}
This method first removes all of the sublayers, which represent the existing LineShape layers. It then creates a deepCopy from the DrawView that’s passed as the source. Lastly, it adds each line to the layer.
Finally, you actually need to call this method whenever the Animate button on screen is pressed. Open ViewController.swift and add the following right after the opening curly brace for animatePressed(_:):
mirrorDrawViews.forEach { $0.copyLines(from: inputDrawView) }
mirrorDrawViews.forEach { $0.animate() }
This first iterates through each mirrorDrawView and copies the inputDrawView. It then calls animate() on each mirrorDrawView to start the animation.
Build and run, draw into the top-left input view, and press Animate.
Key points
You learned about the prototype pattern in this chapter. Here are its key points:
-
The prototype pattern enables an object to copy itself. It involves two types: a copying protocol and a prototype.
-
The copying protocol declares copy methods, and the prototype conforms to the protocol.
-
Foundationprovides anNSCopyingprotocol, but it doesn’t work well in Swift. It’s easy to roll your ownCopyingprotocol, which eliminates reliance onFoundationor any other framework entirely. -
The key to creating a
Copyingprotocol is creating a copy initializer with the forminit(_ prototype:).
In this chapter, you also implemented key functionality in MirrorPad. This is a pretty neat app, but it does have some issues. For example, the app allows you to continue drawing while it’s animating. You could try to hack a solution for this directly within DrawView, but this class is already starting to get messy and hard to maintain. You’ll use another pattern to fix both of these problems: the state pattern.
Continue onto the next chapter to learn about the state design pattern and continue building out MirrorPad!