15.
State Pattern
Written by Joshua Greene
The state pattern is a behavioral pattern that allows an object to change its behavior at runtime. It does so by changing its current state. Here, “state” means the set of data that describes how a given object should behave at a given time.
This pattern involves three types:
-
The context is the object that has a current state and whose behavior changes.
-
The state protocol defines required methods and properties. Developers commonly substitute a base state class in place of a protocol. By doing so, they can define stored properties in the base, which isn’t possible using a protocol.
Even if a base class is used, it’s not intended to be instantiated directly. Rather, it’s defined for the sole purpose of being subclassed. In other languages, this would be an
abstract class. Swift currently doesn’t haveabstractclasses, however, so this class isn’t instantiated by convention only. -
Concrete states conform to the state protocol, or if a base class is used instead, they subclass the base. The context holds onto its current state, but it doesn’t know its concrete state type. Instead, the context changes behavior using polymorphism: concrete states define how the context should act. If you ever need a new behavior, you define a new concrete state.
An important question remains, however: where do you actually put the code to change the context’s current state? Within the context itself, the concrete states, or somewhere else?
You may be surprised to find out that the state pattern doesn’t tell you where to put state change logic! Instead, you’re responsible for deciding this. This is both a strength and weakness of this pattern: It permits designs to be flexible, but at the same time, it doesn’t provide complete guidance on how to implement this pattern.
You’ll learn two ways to implement state changes in this chapter. In the playground example, you’ll put change logic within the context, and in the tutorial project, you’ll let the concrete states themselves handle changes.
When should you use it?
Use the state pattern to create a system that has two or more states that it changes between during its lifetime. The states may be either limited in number (a “closed“ set) or unlimited (an “open” set). For example, a traffic light can be defined using a closed set of “traffic light states.” In the simplest case, it progresses from green to yellow to red to green again.
An animation engine can be defined as an open set of “animation states.” It has unlimited different rotations, translations and other animations that it may progress through during its lifetime.
Both open- and closed-set implementations of the state pattern use polymorphism to change behavior. As a result, you can often eliminate switch and if-else statements using this pattern.
Instead of keeping track of complex conditions within the context, you pass through calls to the current state; you’ll see how this works in both the playground example and tutorial project. If you have a class with several switch or if-else statements, try to define it using the state pattern instead. You’ll likely create a more flexible and easier maintain system as a result.
Playground example
Open IntermediateDesignPatterns.xcworkspace in the Starter directory, and then open the State page.
You’ll implement the “traffic light” system mentioned above. Specifically, you’ll use Core Graphics to draw a traffic light and change its “current state” from green to yellow to red to green again.
Note: You’ll need a basic understanding of Core Graphics to fully understand this playground example. At the very least, you should know a little about
CALayerandCAShapeLayer. If you’re new to Core Graphics, read our free tutorial about it here: (http://bit.ly/rw-coregraphics).
Enter the following after Code example to define the context:
import UIKit
import PlaygroundSupport
// MARK: - Context
public class TrafficLight: UIView {
// MARK: - Instance Properties
// 1
public private(set) var canisterLayers: [CAShapeLayer] = []
// MARK: - Object Lifecycle
// 2
@available(*, unavailable,
message: "Use init(canisterCount: frame:) instead")
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) is not supported")
}
// 3
public init(canisterCount: Int = 3,
frame: CGRect =
CGRect(x: 0, y: 0, width: 160, height: 420)) {
super.init(frame: frame)
backgroundColor =
UIColor(red: 0.86, green: 0.64, blue: 0.25, alpha: 1)
createCanisterLayers(count: canisterCount)
}
// 4
private func createCanisterLayers(count: Int) {
}
}
Here’s what this does:
-
You first define a property for
canisterLayers. This will hold onto the “traffic light canister” layers. These layers will hold onto the green/yellow/red states as sublayers. -
To keep the playground simple, you won’t support
init(coder:). -
You declare
init(canisterCount:frame:)as the designated initializer and provide default values for bothcanisterCountandframe. You also set thebackgroundColorto a yellowish color and callcreateCanisterLayers(count:).
You’ll do the real work within createCanisterLayers(count:). Add the following to this method:
// 1
let paddingPercentage: CGFloat = 0.2
let yTotalPadding = paddingPercentage * bounds.height
let yPadding = yTotalPadding / CGFloat(count + 1)
// 2
let canisterHeight = (bounds.height - yTotalPadding) / CGFloat(count)
let xPadding = (bounds.width - canisterHeight) / 2.0
var canisterFrame = CGRect(x: xPadding,
y: yPadding,
width: canisterHeight,
height: canisterHeight)
// 3
for _ in 0 ..< count {
let canisterShape = CAShapeLayer()
canisterShape.path = UIBezierPath(
ovalIn: canisterFrame).cgPath
canisterShape.fillColor = UIColor.black.cgColor
layer.addSublayer(canisterShape)
canisterLayers.append(canisterShape)
canisterFrame.origin.y += (canisterFrame.height + yPadding)
}
Taking it comment-by-comment:
-
You first calculate
yTotalPaddingas a percentage ofbounds.heightand then use the result to determine eachyPaddingspace. The total number of “padding spaces” is equal tocount(the number of canisters)+ 1(one extra space for the bottom). -
Using
yPadding, you calculatecanisterHeight. To keep the canisters square, you usecanisterHeightfor both the height and width of each canister. You then usecanisterHeightto calculate thexPaddingrequired to center each canister.Ultimately, you use
xPadding,yPaddingandcanisterHeightto createcanisterFrame, which represents the frame for the first canister. -
Using
canisterFrame, you loop from0tocountto create acanisterShapefor the required number of canisters, given bycount. After creating eachcanisterShape, you add it tocanisterLayers. By keeping a reference to each canister layer, you’ll later be able to add “traffic light state“ sublayers to them.
Add the following code to see your code in action:
let trafficLight = TrafficLight()
PlaygroundPage.current.liveView = trafficLight
Here, you create an instance of trafficLight and set it as the liveView for the playground’s current page, which outputs to the Live View. If you don’t see the output, press Editor ▸ Live View.
To prevent compiler errors as you continue modify this class, delete the two lines of code you just added.
To show the light states, you need to define a state protocol. Add the following at the bottom of the playground page:
// MARK: - State Protocol
public protocol TrafficLightState: class {
// MARK: - Properties
// 1
var delay: TimeInterval { get }
// MARK: - Instance Methods
// 2
func apply(to context: TrafficLight)
}
-
You first declare a
delayproperty, which defines the time interval a state should be shown. -
You then declare
apply(to:), which each concrete state will need to implement.
Next, add the following properties to TrafficLight, right after canisterLayers, ignoring the resulting compiler errors for now:
public private(set) var currentState: TrafficLightState
public private(set) var states: [TrafficLightState]
As the names imply, you’ll use currentState to hold onto the traffic light’s current TrafficLightState, and states to hold onto all TrafficLightStates for the traffic light. You denote both of these properties as private(set) to ensure only the TrafficLight itself can set them.
Next, replace init(canisterCount:frame:) with the following:
public init(canisterCount: Int = 3,
frame: CGRect =
CGRect(x: 0, y: 0, width: 160, height: 420),
states: [TrafficLightState]) {
// 1
guard !states.isEmpty else {
fatalError("states should not be empty")
}
self.currentState = states.first!
self.states = states
// 2
super.init(frame: frame)
backgroundColor =
UIColor(red: 0.86, green: 0.64, blue: 0.25, alpha: 1)
createCanisterLayers(count: canisterCount)
}
-
You’ve added
statesto this initializer. Since it doesn’t make logical sense forstatesto be empty, you throw afatalErrorif it is. Otherwise, you set thecurrentStateto thefirstobject withinstatesand setself.statesto the passed-instates. -
Afterwards, you call
super.init, set thebackgroundColorand callcreateCanisterLayers, just as you did before.
Next, add the following code right before the ending class curly brace for TrafficLight:
public func transition(to state: TrafficLightState) {
removeCanisterSublayers()
currentState = state
currentState.apply(to: self)
}
private func removeCanisterSublayers() {
canisterLayers.forEach {
$0.sublayers?.forEach {
$0.removeFromSuperlayer()
}
}
}
You define transition(to state:) to change to a new TrafficLightState. You first call removeCanisterSublayers to remove existing canister sublayers; this ensures a new state isn’t added on top of an existing one. You then set currentState and call apply. This allows the state to add its contents to the TrafficLight instance.
Next, add this line to the end of init(canisterCount:frame:states:):
transition(to: currentState)
This ensures the currentState is added to the view when it’s initialized.
Now you need to create the concrete states. Add the following code to the end of the playground:
// MARK: - Concrete States
public class SolidTrafficLightState {
// MARK: - Properties
public let canisterIndex: Int
public let color: UIColor
public let delay: TimeInterval
// MARK: - Object Lifecycle
public init(canisterIndex: Int,
color: UIColor,
delay: TimeInterval) {
self.canisterIndex = canisterIndex
self.color = color
self.delay = delay
}
}
You declare SolidTrafficLightState to represent a “solid light” state. For example, this could represent a solid green light. This class has three properties: canisterIndex is the index of the canisterLayers on TrafficLight to which this state should be added, color is the color for the state and delay is how long until the next state should be shown.
You next need to make SolidTrafficLightState conform to TrafficLightState. Add the following code to the end of the playground:
extension SolidTrafficLightState: TrafficLightState {
public func apply(to context: TrafficLight) {
let canisterLayer = context.canisterLayers[canisterIndex]
let circleShape = CAShapeLayer()
circleShape.path = canisterLayer.path!
circleShape.fillColor = color.cgColor
circleShape.strokeColor = color.cgColor
canisterLayer.addSublayer(circleShape)
}
}
Within apply(to:), you create a new CAShapeLayer for the state: you set its path to match the canisterLayer for its designated canisterIndex, set its fillPath and strokeColor using its color, and ultimately, add the shape to the canister layer.
Next, add this code to the end of the playground:
// MARK: - Convenience Constructors
extension SolidTrafficLightState {
public class func greenLight(
color: UIColor =
UIColor(red: 0.21, green: 0.78, blue: 0.35, alpha: 1),
canisterIndex: Int = 2,
delay: TimeInterval = 1.0) -> SolidTrafficLightState {
return SolidTrafficLightState(canisterIndex: canisterIndex,
color: color,
delay: delay)
}
public class func yellowLight(
color: UIColor =
UIColor(red: 0.98, green: 0.91, blue: 0.07, alpha: 1),
canisterIndex: Int = 1,
delay: TimeInterval = 0.5) -> SolidTrafficLightState {
return SolidTrafficLightState(canisterIndex: canisterIndex,
color: color,
delay: delay)
}
public class func redLight(
color: UIColor =
UIColor(red: 0.88, green: 0, blue: 0.04, alpha: 1),
canisterIndex: Int = 0,
delay: TimeInterval = 2.0) -> SolidTrafficLightState {
return SolidTrafficLightState(canisterIndex: canisterIndex,
color: color,
delay: delay)
}
}
Here, you add convenience class methods to create common SolidTrafficLightStates: solid green, yellow and red lights.
You’re finally ready to put this code into action! Add the following to the end of the playground:
let greenYellowRed: [SolidTrafficLightState] =
[.greenLight(), .yellowLight(), .redLight()]
let trafficLight = TrafficLight(states: greenYellowRed)
PlaygroundPage.current.liveView = trafficLight
This creates a typical green/yellow/red traffic light and sets it to the current playground page’s liveView.
But wait! Shouldn’t the traffic light be switching from one state to the next? Oh — you haven’t actually implemented this functionality yet. The state pattern doesn’t actually tell you where or how to perform state changes. In this case, you actually have two choices: you can put state change logic within TrafficLight, or you can put this within TrafficLightState.
In a real application, you should evaluate which of these choices is better for your expected use cases and what’s better in the long run. For this playground example, “another developer” (i.e., your humble author) has told you the logic is better suited in the TrafficLight, so this is where you’ll put the changing code.
First, add the following extension after the closing curly brace for TrafficLightState:
// MARK: - Transitioning
extension TrafficLightState {
public func apply(to context: TrafficLight,
after delay: TimeInterval) {
let queue = DispatchQueue.main
let dispatchTime = DispatchTime.now() + delay
queue.asyncAfter(deadline: dispatchTime) {
[weak self, weak context] in
guard let self = self, let context = context else {
return
}
context.transition(to: self)
}
}
}
This extension adds “apply after” functionality to every type that conforms to TrafficLightState. In apply(to:after:), you dispatch to DispatchQueue.main after a passed-in delay, at which point you transition to the current state. In order to break potential retain cycles, you specify both self and context as weak within the closure.
Next, add the following within TrafficLight, right after removeCanisterSublayers():
public var nextState: TrafficLightState {
guard let index = states.firstIndex(where: {
$0 === currentState
}),
index + 1 < states.count else {
return states.first!
}
return states[index + 1]
}
This creates a convenience computed property for the nextState, which you determine by finding the index representing the currentState. If there are states after the index, which you determine by index + 1 < states.count, you return that next state. If there aren’t states after the currentState, you return the first state to go back to the start.
Finally, add the following line to the end of transition(to state:):
nextState.apply(to: self, after: currentState.delay)
This tells the nextState to apply itself to the traffic light after the current state’s delay has passed.
Check out the Assistant editor, and you’ll now see it cycling states!
What should you be careful about?
Be careful about creating tight coupling between the context and concrete states. Will you ever want to reuse the states in a different context? If so, consider putting a protocol between the concrete states and context, instead of having concrete states call methods on a specific context.
If you choose to implement state change logic within the states themselves, be careful about tight coupling from one state to the next.
Will you ever want to transition from state to another state instead? In this case, consider passing in the next state via an initializer or property.
Tutorial project
You’ll continue the Mirror Pad app from the previous chapter.
If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter\MirrorPad\MirrorPad.xcodeproj in Xcode.
Build and run the app, and draw several lines into the top-left view. Then press Animate to watch the app animate the mirrored drawings. Before the animation completes, try drawing more lines into the top-left view. The app lets you do this, but it’s a poor user experience.
Let’s fix that!
Open DrawView.swift and check out this class. It’s currently doing a lot of work: accepting user inputs, performing copying, drawing, animation and more. If you continue to expand Mirror Pad’s functionality over time, you’d likely struggle to maintain this class. It’s simply doing too much!
You’ll fix both of these using the state pattern, but you already guessed that, right? You’ll turn DrawView into the context, create a new DrawViewState as the base state class and create several concrete states that subclass DrawViewState to perform required behavior.
First, you need to add new groups and files. Create a new group called DrawView inside the Views group. Then, move DrawView.swift and LineShape.swift into your newly-created DrawView group.
Create another new group called States inside the DrawView group. Within the States group, create new Swift files for each of these:
- AcceptInputState.swift
- AnimateState.swift
- ClearState.swift
- CopyState.swift
- DrawViewState.swift
Your Views group should now look like this in the File hierarchy:
You’ll next implement DrawViewState. Replace the contents of DrawViewState.swift with the following:
import UIKit
public class DrawViewState {
// MARK: - Class Properties
// 1
public class var identifier: AnyHashable {
return ObjectIdentifier(self)
}
// MARK: - Instance Properties
// 2
public unowned let drawView: DrawView
// MARK: - Object Lifecycle
public init(drawView: DrawView) {
self.drawView = drawView
}
// MARK: - Actions
// 3
public func animate() { }
public func copyLines(from source: DrawView) { }
public func clear() { }
public func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) { }
public func touchesMoved(_ touches: Set<UITouch>,
with event: UIEvent?) { }
// MARK: - State Management
// 4
@discardableResult internal func transitionToState(
matching identifier: AnyHashable) -> DrawViewState {
// TODO: - Implement this
return self
}
}
Here’s what’s going on in the code above:
-
You first declare a class property called
identifier. You’ll later use this to switch between states. -
You then declare an
unownedinstance property calleddrawView, which will be the context in the state pattern. You pass in the context via the designated initializerinit(drawView:).This creates a tight coupling between
DrawViewStateandDrawView. In this app, you’ll only useDrawViewStatealong withDrawView, so this coupling isn’t a problem. In your own app, however, you should consider whether or not you’d ever want to reuseDrawViewStatewith a different context. -
You then declare methods for all of the possible actions and provide empty implementations for each. Concrete state subclasses will need to override whichever actions they support. If a concrete state doesn’t override an action, it will inherit this empty implementation and do nothing.
-
At the end, you declare a method to change between states. This has a return value of
DrawViewStateto enable you to call an action on the new state after switching to it. You need to make changes toDrawViewbefore you can complete this method, however, so you add aTODOcomment and returnselfas a placeholder for now.
You’ll next stub out each of the concrete states. Essentially, you’ll be moving code from DrawView into the states to facilitate the refactoring.
Replace the contents of AcceptInputState.swift with the following:
import UIKit
public class AcceptInputState: DrawViewState {
}
Replace AnimateState.swift’s contents with this:
import UIKit
public class AnimateState: DrawViewState {
}
Replace ClearState.swift’s contents with this:
import UIKit
public class ClearState: DrawViewState {
}
Lastly, replace CopyState.swift’s contents with this:
import UIKit
public class CopyState: DrawViewState {
}
Great! You can now begin to refactor DrawView. Open DrawView.swift and add the following properties, right after the existing ones:
public lazy var currentState =
states[AcceptInputState.identifier]!
public lazy var states = [
AcceptInputState.identifier: AcceptInputState(drawView: self),
AnimateState.identifier: AnimateState(drawView: self),
ClearState.identifier: ClearState(drawView: self),
CopyState.identifier: CopyState(drawView: self)
]
As its name implies, you’ll use currentState to hold onto the current concrete state.
You’ll hold onto all possible states within states. This is a dictionary that uses the computed value from identifier defined on DrawViewState for keys and concrete state instances as values. Why is this a dictionary and not a array? This is because concrete states don’t have one transition order! Rather, state transitions depend on user interaction. Here’s how it will work:
-
The
currentStateis first set toAcceptInputStateas its default value. -
If the user presses Clear,
AcceptInputStatewill change the context’scurrentStatetoClearState; the clear state will perform the “clear” behavior; and afterwards, it will change the context’scurrentStateback toAcceptInputState. -
If the user presses Animate,
AcceptInputStatewill change the context’scurrentStatetoAnimateState; the animate state will perform animations; and upon completion, it will change the context’scurrentStateback toAcceptInputState. -
If
copyis called, theAcceptInputStatewill change the context’scurrentStatetoCopyState; the copy state will perform copying; and afterwards, it will changecurrentStateback toAcceptInputState.
Remember the method you stubbed out on DrawViewState before? Now that DrawView has a currentState and states defined, you can complete this method!
Open DrawViewState.swift and replace the contents of transitionToState(matching:) with the following:
let state = drawView.states[identifier]!
drawView.currentState = state
return state
This looks up the state from drawView.states using the passed-in identifier, sets the value to drawView.currentState and returns the state.
All that’s left to do is move logic from DrawView into the appropriate concrete states.
You’re going to be editing DrawView a lot, so it will be useful to open this in a new Editor window. To do so, hold down Option and left-click on DrawView.swift in the File hierarchy. This will let you easily edit DrawView at the same time as the concrete state classes.
Click anywhere within the first editor window and then left-click AcceptInputState.swift to open it within this window. Add the following methods to this class:
// 1
public override func animate() {
let animateState = transitionToState(
matching: AnimateState.identifier)
animateState.animate()
}
public override func clear() {
let clearState = transitionToState(
matching: ClearState.identifier)
clearState.clear()
}
public override func copyLines(from source: DrawView) {
let copyState = transitionToState(
matching: CopyState.identifier)
copyState.copyLines(from: source)
}
// 2
public override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: drawView) else {
return
}
let line = LineShape(color: drawView.lineColor,
width: drawView.lineWidth,
startPoint: point)
drawView.lines.append(line)
drawView.layer.addSublayer(line)
}
public override func touchesMoved(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: drawView),
drawView.bounds.contains(point),
let currentLine = drawView.lines.last else { return }
currentLine.addPoint(point)
}
Here’s the play-by-play:
-
animate(),clear()andcopyLines(from:)are very similar. YoutransitionToState(matching:)to change to the appropriate state and simply forward the call onto it. -
AcceptInputStateis responsible for handlingtouchesBegan(_:with:)andtouchesMoved(_:with:)itself. If you compare this code to the code withinDrawView, you’ll see it’s nearly identical. The only difference is you sometimes have to prefix calls todrawView.to perform operations ondrawViewinstead of the state.
Replace touchesBegan(_:with:) and touchesMoved(_:with:) within DrawView with the following:
public override func touchesBegan(_ touches: Set<UITouch>,
with event: UIEvent?) {
currentState.touchesBegan(touches, with: event)
}
public override func touchesMoved(_ touches: Set<UITouch>,
with event: UIEvent?) {
currentState.touchesMoved(touches, with: event)
}
Here you simply forward these method calls onto the currentState. If the currentState is an instance of AcceptInputState, which it is by default, the app will behave exactly as before.
Build and run and draw into the top-left view to verify the app still works as expected.
Next, open AnimateState.swift and add these methods to the class:
public override func animate() {
guard let sublayers = drawView.layer.sublayers,
sublayers.count > 0 else {
// 1
transitionToState(
matching: AcceptInputState.identifier)
return
}
sublayers.forEach { $0.removeAllAnimations() }
UIView.animate(withDuration: 0.3) {
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
// 2
self?.transitionToState(
matching: AcceptInputState.identifier)
}
self.setSublayersStrokeEnd(to: 0.0)
self.animateStrokeEnds(of: sublayers, at: 0)
CATransaction.commit()
}
}
private func setSublayersStrokeEnd(to value: CGFloat) {
drawView.layer.sublayers?.forEach {
guard let shapeLayer = $0 as? CAShapeLayer else { return }
shapeLayer.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = value
animation.toValue = value
animation.fillMode = .forwards
shapeLayer.add(animation, forKey: nil)
}
}
private func animateStrokeEnds(of layers: [CALayer], at index: Int) {
guard index < layers.count else { return }
let currentLayer = layers[index]
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
currentLayer.removeAllAnimations()
self?.animateStrokeEnds(of: layers, at: index + 1)
}
if let shapeLayer = currentLayer as? CAShapeLayer {
shapeLayer.strokeEnd = 1.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 1.0
animation.fillMode = .forwards
animation.fromValue = 0.0
animation.toValue = 1.0
shapeLayer.add(animation, forKey: nil)
}
CATransaction.commit()
}
This code is nearly identical to DrawView, but there are two main changes:
-
If there aren’t any
sublayersto animate, you immediately transition back toAcceptInputStatewithout doing anything else. -
Whenever the entire animation is complete, you likewise transition back to
AcceptInputState.
You should also take note of the methods that you didn’t override, especially touchesBegan(_:with:) and touchesMoved(_:with:). Consequently, whenever the currentState is set to AnimateState, you won’t do anything if the user attempts to draw into the view. Essentially, you fixed a bug by doing nothing. How awesome is that!
Of course, you need to make sure DrawView passes the call to its animate() onto the currentState instead. Thereby, replace animate() within DrawView with this:
public func animate() {
currentState.animate()
}
Then, delete setSublayersStrokeEnd() and animateStrokeEnds() from DrawView; you don’t need these methods anymore since the logic is now handled within AnimateState.
You have just two more states to go! Open ClearState.swift, and add the following method to the class:
public override func clear() {
drawView.lines = []
drawView.layer.sublayers?.removeAll()
transitionToState(matching: AcceptInputState.identifier)
}
This is just like DrawView’s code. The only addition is that once “clearing” is complete, you transition back to AcceptInputState.
You also need to update DrawView; replace its clear() with this instead:
public func clear() {
currentState.clear()
}
Open CopyState.swift from the File hierarchy, and add this method within the class:
public override func copyLines(from source: DrawView) {
drawView.layer.sublayers?.removeAll()
drawView.lines = source.lines.deepCopy()
drawView.lines.forEach { drawView.layer.addSublayer($0) }
transitionToState(matching: AcceptInputState.identifier)
}
Again, this is just like DrawView, and the only addition is that you transition back to AcceptInputState once copying is complete.
Of course, you also need to update copyLines(from:) within DrawView with this:
public func copyLines(from source: DrawView) {
currentState.copyLines(from: source)
}
Build and run, and validate that everything works as it did before.
Take a look at how much shorter DrawView is now! You’ve shifted its responsibilities to its concrete states instead. And if you ever wanted to add new logic, you’d simply create a new DrawViewState.
Key points
You learned about the state pattern in this chapter. Here are its key points:
-
The state pattern permits an object to change its behavior at runtime. It involves three types: the context, state protocol and concrete states.
-
The context is the object that has a current state; the state protocol defines required methods and properties; and the concrete states implement the state protocol and actual behavior that changes at runtime.
-
The state pattern doesn’t actually tell you where to put transition logic between states. Rather, this is left for you to decide: you can put this logic either within the context or within the concrete states.
Mirror Pad is also really coming along! It’s pretty cool that you can see the drawings get rendered when you press “Animate.” However, wouldn’t it be better if you could also see them added in real time while you draw? You bet it would!
Continue onto the next chapter to learn about the multicast delegate design pattern and add the above real-time feature to Mirror Pad!