Skip to content

Lang-8/SwiftState

This branch is up to date with ReactKit/SwiftState:swift/2.0.

Folders and files

NameName
Last commit message
Last commit date
Dec 8, 2015
Dec 8, 2015
Nov 30, 2015
Mar 21, 2016
Aug 2, 2016
Mar 21, 2016
Feb 24, 2016
Feb 24, 2016
Dec 8, 2015
Mar 8, 2016
Dec 8, 2015
Dec 8, 2015
Aug 9, 2014
Feb 23, 2016
Jan 13, 2016
Feb 24, 2016
Feb 23, 2016

Repository files navigation

SwiftState Circle CI

Elegant state machine for Swift.

SwiftState

Example

enum MyState: StateType {
    case State0, State1, State2
}
// setup state machine
let machine = StateMachine<MyState, NoEvent>(state: .State0) { machine in
    
    machine.addRoute(.State0 => .State1)
    machine.addRoute(.Any => .State2) { context in print("Any => 2, msg=\(context.userInfo)") }
    machine.addRoute(.State2 => .Any) { context in print("2 => Any, msg=\(context.userInfo)") }
    
    // add handler (`context = (event, fromState, toState, userInfo)`)
    machine.addHandler(.State0 => .State1) { context in
        print("0 => 1")
    }
    
    // add errorHandler
    machine.addErrorHandler { event, fromState, toState, userInfo in
        print("[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 => State0

Transition by Event

Use <-! operator to try transition by Event rather than specifying target State.

enum MyEvent: EventType {
    case Event0, Event1
}
let machine = 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 in
        print(".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.

State & Event enums with associated values

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:

enum StrState: StateType {
    case Str(String) ...
}
enum StrEvent: EventType {
    case Str(String) ...
}

let machine = Machine<StrState, StrEvent>(state: .Str("initial")) { machine in
    
    machine.addRouteMapping { event, fromState, userInfo -> StrState? in
        // no route for no-event
        guard let event = event else { return nil }
        
        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:
                return nil
        }
    }
    
}

// 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.

Features

  • Easy Swift syntax
    • Transition: .State0 => .State1, [.State0, .State1] => .State2
    • Try state: machine <- .State1
    • Try state + messaging: machine <- (.State1, "GoGoGo")
    • Try event: machine <-! .Event1
  • Highly flexible transition routing
    • Using Condition

    • Using .Any state

      • Entry handling: .Any => .SomeState
      • Exit handling: .SomeState => .Any
      • Blacklisting: .Any => .Any + Condition
    • Using .Any event

    • Route 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

Terms

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

Related Articles

  1. Swiftで有限オートマトン(ステートマシン)を作る - Qiita (Japanese)
  2. Swift+有限オートマトンでPromiseを拡張する - Qiita (Japanese)

Licence

MIT

About

Elegant state machine for Swift.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 96.0%
  • Ruby 3.7%
  • Objective-C 0.3%