Dev notebook: an enum alternative to GKState / GKStateMachine

I worked on my first GameplayKit project earlier this year and there were things I liked about the lightweight, general state-machine model provided by the GKState and GKStateMachine classes. However I also found a few odd patterns in these classes: their implementation without using enumeration values for states, the inelegant (StateClass).class.self syntax model required by the state machine enterState() method, and the update() interface which I found extra and somewhat orthogonal to the pure function of the state machine (I keep time-based concerns in entities and components).

In my current project, which is not a game, I wanted to use a pattern similar to GKStateMachine for various UI and model states, but decided to implement a simple framework that addressed these issues rather than use GKStateMachine.

The basic pattern is provided by a protocol that *could* be implemented by any class, but the interface strongly suggests that a state will be an enum : Int type:

protocol MPState {
    var rawEnumValue : Int { get }
    var validNextStates : [Int] { get }
    
    func didEnter(from prevState: MPState?, with machine: MPStateMachine)
    func willExit(to nextState: MPState?, with machine: MPStateMachine)
}

Obviously the rawEnumValue will be very easy to implement as a wrapper on rawValue if the state is implemented as an enum : Int rather than some other type of struct or class. Also, the required validNextStates computed property is expected to return values that can be compared with rawEnumValue for inclusion/exclusion, and this logic is made explicit in an extension to the protocol (which also implements didEnter and willExit as no-ops to make them optional):

extension MPState {
    func isValidNextState(_ nextState : MPState
                          with machine: MPStateMachine) -> Bool {

        return self.validNextStates.contains(nextState.rawEnumValue)
    }
    
    func didEnter(from prevState: MPState?, with machine: MPStateMachine) {
        // no-op, override to handle
    }
    
    func willExit(to nextState: MPState?, with machine: MPStateMachine) {
        // no-op, override to handle
    }
}

One thing this pattern precludes: the state object itself can’t retain any persistent reference to either the state machine or any model object, since enumerations in swift can’t have stored properties. This is handled by simply passing the machine object into the three relevant methods; the machine is a full-fledged class which also has an optional model property that can bring in more object context.

The state-machine class provides an interface like that of GKStateMachine, but instead of passing a class to the enter() method you can just pass your enumerated state value. It has a similar currentState read-only computed property (returning a private machineState value which can only be updated by successful enter). Here’s the entire implementation:

class MPStateMachine : NSObject {
    var model: Any? = nil
    
    var currentState : MPState? {
        get {
            return self.machineState
        }
    }
    
    func canEnterState(_ nextState : MPState) -> Bool {
        if nil != self.machineState {
            return self.machineState!.isValidNextState(nextState, with: self)
        }
        return true // assume we can enter any state from nil
    }
    
    func enter(_ state: MPState) -> Bool {
        var setState : Bool = false
        if self.machineState == nil {
            self.machineState = state
            state.didEnter(from: nil, with: self)
            setState = true
        } else if self.machineState!.isValidNextState(state, with: self) {
            let oldState = self.machineState!
            self.machineState?.willExit(to: state, with: self)
            self.machineState = state
            state.didEnter(from: oldState, with: self)
            setState = true
        }
        return setState
    }
    
    fileprivate var machineState : MPState? = nil

}

The model property in the state machine is up to the client code to define; it will typically be the model object whose state is being managed, i.e. the domain-specific owner of the state machine instance.

As a sample implementation I created an MPApplicationState which tracks the app states of starting, running, terminating, owned by my AppDelegate class. This demonstrates the pattern of how valid next-states can be handled by a simple switch statement for most state collections, but you could always add helper methods for various state-transition contexts for more complex cases that need to inspect the model:

enum MPApplicationState : Int, MPState {

    case starting
    case running
    case terminating
    
    var rawEnumValue: Int {
        get {
            return self.rawValue
        }
    }
    
    var validNextStates: [Int] {
        get {
            var nextStates : [Int] = []
            switch self {
                case .starting:
                    nextStates.append(MPApplicationState.running.rawValue)
                    nextStates.append(MPApplicationState.terminating.rawValue)
                case .running:
                    nextStates.append(MPApplicationState.terminating.rawValue)
                case .terminating:
                    nextStates.removeAll()
            }
            return nextStates
        }
    }
    
    func didEnter(from prevState: MPState?, with machine: MPStateMachine) {
        print (“*** MPApplicationState *** did enter state: (String(describing:self)) from (String(describing:prevState)))
    }
    
    func willExit(to nextState: MPState, with machine: MPStateMachine) {
        print (“*** MPApplicationState *** will exit state: (String(describing:self)) to state: (String(describing:nextState)))
    }

}

Just to show the functionality (not really because it’s useful) I created an MPStateMachine property on my AppDelegate (allocated at the declaration), and transition states in the following method overrides:

func applicationWillFinishLaunching(_ notification: Notification) {
    _ = self.stateMachine.enter(MPApplicationState.starting)

}
func applicationDidFinishLaunching(_ aNotification: Notification) {
    _ = self.stateMachine.enter(MPApplicationState.running)
    // etc…
}

func applicationWillTerminate(_ aNotification: Notification) {

    _ = self.stateMachine.enter(MPApplicationState.terminating)
}

I’m not bothering to check the output values for these simple cases but the idea is that more involved state transitions could do so, and/or pre-check the ability of the state machine to transition with a call a like:

   self.stateMachine.canEnterState(MPApplicationState.running)

And now checking the current application state looks a bit nicer than it would with GKStateMachine:

    if appDelegate.stateMachine.currentState != MPApplicationState.terminating {

        // do something…

That’s it!