TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Oct 2015 |
SPMSupports SPM | ✗ |
Maintained by Johan Kool, Johan Kool.
A Simple State Machine in Swift
Highlights of this state machine:
This state machine is typically setup with an enum for its possible states, and an enum for its actions. The state of the machine determines wether an action is allowed to run. The state of a machine can only be changed via an action. The action handler returns the new state of the machine.
It is also possible to register multiple handlers that get run when certain state changes occur.
You need at least iOS 8.0 or Mac OS X 10.9.
This implementation is written using Swift 2.0. Use version 0.1.5 if you still need support for Swift 1.2.
The API is fully documented in the source. See also the example below.
To install using CocoaPods, add to your Podfile:
pod 'EFStateMachine', '~> 0.2'
Or you just compile the source and add the StateMachine.framework
to your own project.
Say you want to create the state machine to capture the flow in this diagram:
First, create an enum
to hold the states. It should conform to the Hashable
protocol. Using a String
typed enum
is recommended.
enum LoadState: String {
case Empty
case Loading
case Complete
case Failed
}
The enum
for the actions is declared the same way.
enum LoadAction: String {
case Load
case FinishLoading
case Cancel
case Reset
}
Now that we have both the states and actions declared, we can create the machine and give it its initial state.
let machine = StateMachine<LoadState, LoadAction>(initialState: .Empty)
For each action, register a handler to run. The handler will only be run if the current state of the state machine is one of those listed in fromStates
. The handler must return a state, which will become the new state of the state machine.
machine.registerAction(.Load, fromStates: [.Empty, .Failed], toStates: [.Loading]) { (machine) -> LoadState in
return .Loading
}
machine.registerAction(.FinishLoading, fromStates: [.Loading], toStates: [.Complete, .Failed]) { (machine) -> LoadState in
return .Complete // (or return .Failed if that's the case)
}
machine.registerAction(.Reset, fromStates: [.Complete, .Failed], toStates: [.Empty]) { (machine) -> LoadState in
return .Empty
}
Because the state machine keeps track of its state history, you can implement methods to handle cancelling an asynchronous task.
machine.registerAction(.Cancel, fromStates: [.Loading], toStates: [.Empty, .Failed]) { (machine) -> LoadState in
return machine.history[machine.history.count - 2]
}
You can also observe state changes and act upon such events. This is also a good moment to point out that you should be careful not to introduce retain loops. If self
holds the state machine, you should use [unowned self
or [weak self]
with your handler.
machine.onChange(toStates: [.Complete]) { [unowned self] (machine, oldState, newState) -> Void in
self.infoLabel.text = "Complete!"
}
Now your state machine is ready to be used. Performing an action is straightforward. If you get back a state, you know the action was run, if nil
is returned the action was ignored.
// Start loading
machine.performAction(.Load) // returns .Loading
// Loading finished
machine.performAction(.FinishLoading) // returns .Complete and updates infoLabel to "Complete!"
// Try loading again (an invalid action)
machine.performAction(.Load) // returns nil
To get a flow diagram like shown above, you save the string returned by the flowdiagramRepresentation
property to a dot file. You can render the diagram with the free app GraphViz.
do {
try machine.saveFlowdiagramRepresentationToPath("/path/to/example-flow-diagram.dot")
} catch let error {
NSLog("Could not save flowdiagram: \(error)")
}
This creates a file containing:
digraph {
graph [rankdir=TB]
0 [label="", shape=plaintext]
0 -> 1
# node
1 [label="Empty", shape=box]
2 [label="Loading", shape=box]
3 [label="Cancel", shape=oval]
4 [label="Load", shape=oval]
5 [label="Complete", shape=box]
6 [label="FinishLoading", shape=oval]
7 [label="Failed", shape=box]
8 [label="Reset", shape=oval]
# links
2 -> 3 [arrowhead=none]
3 -> 1
1 -> 4 [arrowhead=none]
4 -> 2
2 -> 6 [arrowhead=none]
6 -> 5
6 -> 7
5 -> 8 [arrowhead=none]
8 -> 1
7 -> 8 [arrowhead=none]
}