The easiest State Machine in Swift
Here's another article of the serie "The easiest <something>". Previous ones on Core Data (The easiest Core Data) and Future and Promises (The easiest promises in Swift).
It was a cold Sunday afternoon when I decided to bring to Swift my ADBStateMachine implemented in Objective-C right about 5 years ago. Actually, it was... today, 16/12/2018.
Stateful 🦜
Here it is Stateful, a minimalistic, thread-safe, non-boilerplate and super easy to use state machine.
Stateful is available through CocoaPods and under the MIT license.
What is a state machine
I'm pretty much assuming the reader knows what a state machine is. It's is a mathematical model of computation, an abstract concept whereby the machine can have different states, but at a given time fulfills only one of them.
- State machines have an initial state
- Other states can be reached via transitions from previous states
- Transitions are performed in reaction to events
Example
Let's see how to user Stateful with a concrete example.
First, you should define the events and statuses you need. From the image above:
enum EventType {
case click
case success
case failure
case retry
}
enum StateType {
case idle
case fetching
case error
}
Create a state machine with the initial state (you might want to retain it in a property)
let stateMachine = StateMachine<StateType, EventType>(initialState: StateType.idle)
StateMachine
will use the main queue to execute the transition pre and post blocks but you can optionally provide a custom one.
let dispatchQueue = DispatchQueue(label: "com.albertodebortoli.someSerialCallbackQueue")
let stateMachine = StateMachine<StateType, EventType>(initialState: StateType.idle, callbackQueue: dispatchQueue)
Create transitions and add them to the state machine (the state machine will automatically recognize the new states).
let t1 = Transition<StateType, EventType>(with: .click,
fromState: .idle,
toState: .fetching)
let t2 = Transition<StateType, EventType>(with: .success,
from: .fetching,
to: .idle)
let t3 = Transition<StateType, EventType>(with: .failure,
fromState: .fetching,
toState: .error,
preBlock: {
print("Going to move from \(StateType.fetching) to \(StateType.error)!")
}, postBlock: {
print("Just moved from \(StateType.fetching) to \(StateType.error)!")
})
let t4 = Transition<StateType, EventType>(with: .retry,
fromState: .error,
toState: .fetching)
stateMachine.add(transition: t1)
stateMachine.add(transition: t2)
stateMachine.add(transition: t3)
stateMachine.add(transition: t4)
Process events like so
stateMachine.process(event: .start)
stateMachine.process(event: .pause, callback: { result in
switch result {
case .success:
print("Event 'pause' was processed")
case .failure:
print("Event 'pause' cannot currently be processed.")
}
})
Logging
You can optionally enable logging to print extra state change information on the console
stateMachine.enableLogging = true
Example:
[Stateful 🦜]: Processing event 'start' from 'idle'
[Stateful 🦜]: Processed pre condition for event 'start' from 'idle' to 'started'
[Stateful 🦜]: Processed state change from 'idle' to 'started'
[Stateful 🦜]: Processed post condition for event 'start' from 'idle' to 'started'
[Stateful 🦜]: Processing event 'stop' from 'started'
[Stateful 🦜]: Processed pre condition for event 'stop' from 'started' to 'idle'
[Stateful 🦜]: Processed state change from 'started' to 'idle'
[Stateful 🦜]: Processed post condition for event 'stop' from 'started' to 'idle'
Conclusions
State machines come handy even to these days when the world seems to have moved to a more stateless way of doing things. Whenever you need to explicitly surface statuses, transition to a different state as a reaction of an event, and possibly leter recover state, a finite state machine might come handy.
Good examples that come to mind are:
- manage overall application state
- ease deeplinking implementation
- ease UI testing
🦜🦜🦜
Special thanks to Matteo Comisso for contributing to Stateful adding support for Generics.