Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

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:

  1. The context is the object that has a current state and whose behavior changes.

  2. 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 have abstract classes, however, so this class isn’t instantiated by convention only.

  3. 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 CALayer and CAShapeLayer. 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:

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

  2. To keep the playground simple, you won’t support init(coder:).

  3. You declare init(canisterCount:frame:) as the designated initializer and provide default values for both canisterCount and frame. You also set the backgroundColor to a yellowish color and call createCanisterLayers(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:

  1. You first calculate yTotalPadding as a percentage of bounds.height and then use the result to determine each yPadding space. The total number of “padding spaces” is equal to count (the number of canisters) + 1 (one extra space for the bottom).

  2. Using yPadding, you calculate canisterHeight. To keep the canisters square, you use canisterHeight for both the height and width of each canister. You then use canisterHeight to calculate the xPadding required to center each canister.

    Ultimately, you use xPadding, yPadding and canisterHeight to create canisterFrame, which represents the frame for the first canister.

  3. Using canisterFrame, you loop from 0 to count to create a canisterShape for the required number of canisters, given by count. After creating each canisterShape, you add it to canisterLayers. 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)
}
  1. You first declare a delay property, which defines the time interval a state should be shown.

  2. 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)
}
  1. You’ve added states to this initializer. Since it doesn’t make logical sense for states to be empty, you throw a fatalError if it is. Otherwise, you set the currentState to the first object within states and set self.states to the passed-in states.

  2. Afterwards, you call super.init, set the backgroundColor and call createCanisterLayers, 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:

  1. You first declare a class property called identifier. You’ll later use this to switch between states.

  2. You then declare an unowned instance property called drawView, which will be the context in the state pattern. You pass in the context via the designated initializer init(drawView:).

    This creates a tight coupling between DrawViewState and DrawView. In this app, you’ll only use DrawViewState along with DrawView, so this coupling isn’t a problem. In your own app, however, you should consider whether or not you’d ever want to reuse DrawViewState with a different context.

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

  4. At the end, you declare a method to change between states. This has a return value of DrawViewState to enable you to call an action on the new state after switching to it. You need to make changes to DrawView before you can complete this method, however, so you add a TODO comment and return self as 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:

  1. The currentState is first set to AcceptInputState as its default value.

  2. If the user presses Clear, AcceptInputState will change the context’s currentState to ClearState; the clear state will perform the “clear” behavior; and afterwards, it will change the context’s currentState back to AcceptInputState.

  3. If the user presses Animate, AcceptInputState will change the context’s currentState to AnimateState; the animate state will perform animations; and upon completion, it will change the context’s currentState back to AcceptInputState.

  4. If copy is called, the AcceptInputState will change the context’s currentState to CopyState; the copy state will perform copying; and afterwards, it will change currentState back to AcceptInputState.

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:

  1. animate(), clear() and copyLines(from:) are very similar. You transitionToState(matching:) to change to the appropriate state and simply forward the call onto it.

  2. AcceptInputState is responsible for handling touchesBegan(_:with:) and touchesMoved(_:with:) itself. If you compare this code to the code within DrawView, you’ll see it’s nearly identical. The only difference is you sometimes have to prefix calls to drawView. to perform operations on drawView instead 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:

  1. If there aren’t any sublayers to animate, you immediately transition back to AcceptInputState without doing anything else.

  2. 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!

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.
© 2026 Kodeco Inc.