State – Design Patterns in Swift

Introduction

Implementing state-specific behavior directly within a class is inflexible because it commits the class to a particular behavior and makes it impossible to add a new state or change the behavior of an existing state later independently from (without changing) the class. The state pattern solves two problems. One is an object should change its behavior when its internal state changes. The other is State-specific behavior should be defined independently. That is, adding new states should not affect the behavior of existing states.

State Pattern – UML

Use State Pattern in Swift

The Context defines the interface of interest to clients. It also maintains a reference to an instance of a State subclass, which represents the current state of the Context.

import XCTest

class Context {

    private var state: State

    init(_ state: State) {
        self.state = state
        transitionTo(state: state)
    }

    func transitionTo(state: State) {
        print("Context: Transition to " + String(describing: state))
        self.state = state
        self.state.update(context: self)
    }

    func request1() {
        state.handle1()
    }

    func request2() {
        state.handle2()
    }
}

The Context allows changing the State object at runtime. And it delegates part of its behavior to the current State object.

The base State class declares methods that all Concrete State should implement and also provides a backreference to the Context object, associated with the State. This backreference can be used by States to transition the Context to another State.

protocol State: class {

    func update(context: Context)

    func handle1()
    func handle2()
}

class BaseState: State {

    private(set) weak var context: Context?

    func update(context: Context) {
        self.context = context
    }

    func handle1() {}
    func handle2() {}
}

Concrete States implement various behaviors, associated with a state of the Context.

class ConcreteStateA: BaseState {

    override func handle1() {
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.\n")
        context?.transitionTo(state: ConcreteStateB())
    }

    override func handle2() {
        print("ConcreteStateA handles request2.\n")
    }
}

class ConcreteStateB: BaseState {

    override func handle1() {
        print("ConcreteStateB handles request1.\n")
    }

    override func handle2() {
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.\n")
        context?.transitionTo(state: ConcreteStateA())
    }
}

Let’s see how it all works together.

class StateConceptual: XCTestCase {

    func test() {
        let context = Context(ConcreteStateA())
        context.request1()
        context.request2()
    }
}

Conclusion

In summary, the State pattern can be recognized by methods that change their behavior depending on the objects’ state, controlled externally. The State pattern is commonly used in Swift to convert massive switch-base state machines into the objects.

Leave a Reply

Your email address will not be published. Required fields are marked *