Elegant state machine for Swift.
enumMyState:StateType{case state0, state1, state2 }// setup state machine letmachine=StateMachine<MyState,NoEvent>(state:.state0){ machine in machine.addRoute(.state0 =>.state1) machine.addRoute(.any =>.state2){ context inprint("Any => 2, msg=\(context.userInfo)")} machine.addRoute(.state2 =>.any){ context inprint("2 => Any, msg=\(context.userInfo)")} // add handler (`context = (event, fromState, toState, userInfo)`) machine.addHandler(.state0 =>.state1){ context inprint("0 => 1")} // add errorHandler machine.addErrorHandler{ event, fromState, toState, userInfo inprint("[ERROR] \(fromState) => \(toState)")}} // initial XCTAssertEqual(machine.state,MyState.state0) // tryState 0 => 1 => 2 => 1 => 0 machine <-.state1 XCTAssertEqual(machine.state,MyState.state1) machine <-(.state2,"Hello")XCTAssertEqual(machine.state,MyState.state2) machine <-(.state1,"Bye")XCTAssertEqual(machine.state,MyState.state1) machine <-.state0 // fail: no 1 => 0 XCTAssertEqual(machine.state,MyState.state1)This will print:
0=>1 Any =>2, msg=Optional("Hello")2=> Any, msg=Optional("Bye")[ERROR] state1=> state0Use <-! operator to try transition by Event rather than specifying target State.
enumMyEvent:EventType{case event0, event1 }letmachine=StateMachine<MyState,MyEvent>(state:.state0){ machine in // add 0 => 1 => 2 machine.addRoutes(event:.event0, transitions:[.state0 =>.state1,.state1 =>.state2,]) // add event handler machine.addHandler(event:.event0){ context inprint(".event0 triggered!")}} // initial XCTAssertEqual(machine.state,MyState.state0) // tryEvent machine <-!.event0 XCTAssertEqual(machine.state,MyState.state1) // tryEvent machine <-!.event0 XCTAssertEqual(machine.state,MyState.state2) // tryEvent (fails) machine <-!.event0 XCTAssertEqual(machine.state,MyState.state2,"event0 doesn't have 2 => Any")If there is no Event-based transition, use built-in NoEvent instead.
Above examples use arrow-style routing which are easy to understand, but it lacks in ability to handle state & event enums with associated values. In such cases, use either of the following functions to apply closure-style routing:
machine.addRouteMapping(routeMapping)RouteMapping:(_ event: E?, _ fromState: S, _ userInfo: Any?) -> S?
machine.addStateRouteMapping(stateRouteMapping)StateRouteMapping:(_ fromState: S, _ userInfo: Any?) -> [S]?
For example:
enumStrState:StateType{case str(String)...}enumStrEvent:EventType{case str(String)...}letmachine=Machine<StrState,StrEvent>(state:.str("initial")){ machine in machine.addRouteMapping{ event, fromState, userInfo ->StrState?in // no route for no-event guardlet event = event else{returnnil}switch(event, fromState){case(.str("gogogo"),.str("initial")):return.str("Phase 1")case(.str("gogogo"),.str("Phase 1")):return.str("Phase 2")case(.str("finish"),.str("Phase 2")):return.str("end")default:returnnil}}} // initial XCTAssertEqual(machine.state,StrState.str("initial")) // tryEvent (fails) machine <-!.str("go?")XCTAssertEqual(machine.state,StrState.str("initial"),"No change.") // tryEvent machine <-!.str("gogogo")XCTAssertEqual(machine.state,StrState.str("Phase 1")) // tryEvent (fails) machine <-!.str("finish")XCTAssertEqual(machine.state,StrState.str("Phase 1"),"No change.") // tryEvent machine <-!.str("gogogo")XCTAssertEqual(machine.state,StrState.str("Phase 2")) // tryEvent (fails) machine <-!.str("gogogo")XCTAssertEqual(machine.state,StrState.str("Phase 2"),"No change.") // tryEvent machine <-!.str("finish")XCTAssertEqual(machine.state,StrState.str("end"))This behaves very similar to JavaScript's safe state-container rackt/Redux, where RouteMapping can be interpretted as Redux.Reducer.
For more examples, please see XCTest cases.
- Easy Swift syntax
- Transition:
.state0 => .state1,[.state0, .state1] => .state2 - Try state:
machine <- .state1 - Try state + messaging:
machine <- (.state1, "GoGoGo") - Try event:
machine <-! .event1
- Transition:
- Highly flexible transition routing
Using
ConditionUsing
.anystate- Entry handling:
.any => .someState - Exit handling:
.someState => .any - Blacklisting:
.any => .any+Condition
- Entry handling:
Using
.anyeventRoute Mapping (closure-based routing): #36
- Success/Error handlers with
order: UInt8(more flexible than before/after handlers) - Removable routes and handlers using
Disposable - Route Chaining:
.state0 => .state1 => .state2 - Hierarchical State Machine: #10
| Term | Type | Description |
|---|---|---|
| State | StateType (protocol) | Mostly enum, describing each state e.g. .state0. |
| Event | EventType (protocol) | Name for route-group. Transition can be fired via Event instead of explicitly targeting next State. |
| State Machine | Machine | State transition manager which can register Route/RouteMapping and Handler separately for variety of transitions. |
| Transition | Transition | From- and to- states represented as .state1 => .state2. Also, .any can be used to represent any state. |
| Route | Route | Transition + Condition. |
| Condition | Context -> Bool | Closure for validating transition. If condition returns false, transition will fail and associated handlers will not be invoked. |
| Route Mapping | (event: E?, fromState: S, userInfo: Any?) -> S? | Another way of defining routes using closure instead of transition arrows (=>). This is useful when state & event are enum with associated values. Return value (S?) means preferred-toState, where passing nil means no routes available. See #36 for more info. |
| State Route Mapping | (fromState: S, userInfo: Any?) -> [S]? | Another way of defining routes using closure instead of transition arrows (=>). This is useful when state is enum with associated values. Return value ([S]?) means multiple toStates from single fromState (synonym for multiple routing e.g. .state0 => [.state1, .state2]). See #36 for more info. |
| Handler | Context -> Void | Transition callback invoked when state has been changed successfully. |
| Context | (event: E?, fromState: S, toState: S, userInfo: Any?) | Closure argument for Condition & Handler. |
| Chain | TransitionChain / RouteChain | Group of continuous routes represented as .state1 => .state2 => .state3 |
- Swiftで有限オートマトン(ステートマシン)を作る - Qiita (Japanese)
- Swift+有限オートマトンでPromiseを拡張する - Qiita (Japanese)
