Elegant state machine for Swift.

Overview

SwiftState

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

Comments
  • Refactor code for better typing, naming, and RouteMapping support.

    Refactor code for better typing, naming, and RouteMapping support.

    This is a BREAKING CHANGE and release candidate for next version 4.0.0, greatly improved typing and RouteMapping support (handling states/events with associated values).

    See Also: https://github.com/ReactKit/SwiftState/issues/34

    Simple Class Names

    I renamed many verbose class names e.g. StateTransition to simple Transition. (Even if the name collides with other frameworks, we can just simply add framework-prefix e.g. SwiftState.Transition to avoid it.)

    Better Typing

    As requested in https://github.com/ReactKit/SwiftState/issues/34 by @frogcjn, current version 3.x needs to use StateType/EventType which conforms to NilLiteralConvertible to represent .Any state/event, but it was quite ugly because we needed to add extra case Any in their enums.

    To alleviate this, I added extra enum wrappers (State<S> & Event<E>) so that users no longer need to implement .Any anymore.

    RouteMapping

    RouteMapping and StateRouteMapping are new ways of adding routes which has a closure type

    public typealias RouteMapping = (event: E?, fromState: S, userInfo: Any?) -> S?
    public typealias StateRouteMapping = (fromState: S, userInfo: Any?) -> [S]?
    

    to navigate StateMachine to preferred toState without using traditional arrow style, e.g. fromState => toState. These closures help resolving states & events with associated values.

    Please see ~~FrogcjnTest.swift#L124-L180~~ RouteMappingTests.swift#L77-L133 for more details.

    Simple Machine class (works like Redux)

    StateMachine (which can tryState() as well as tryEvent()) is now a subclass of simpler class Machine which is capable of tryEvent() only. By splitting these codes, along with RouteMapping, Machine will now behave like rackt/redux in JavaScript, a safe state container. For more information on Redux, see http://redux.js.org/ .

    Easy Disposable of routes & handlers

    There were previously RouteID and HandlerID being passed as return value for asking StateMachine to unregister, but now Disposable (derived from ReactiveCocoa) will take care of it.

    Others

    • Support watchOS & tvOS.
    opened by inamiy 10
  • suggestion on transition and condition, which should include Event

    suggestion on transition and condition, which should include Event

    There is a case that the event has associate values which cannot be enumerated all the possible values, so that it is not possible to add all Event Route into State Machine.

    In this case, the only way to do it is to use condition.

    But unfortunately, condition only includes transition which has no information about the event.

    So I suggest to add Event information into transition.

    enhancement 
    opened by frogcjn 10
  • Can't use event handler by itself

    Can't use event handler by itself

    Why can't I use the event handler by itself without adding a route? Shouldn't this be a design decision up to the client programmer?

    machine.addEventHandler(.GotoPreviousCard, handler: { (event, transition, order, userInfo) -> Void in
      println("FeedView ! GotoPreviousCard")
    })
    

    Instead, I have to add a route that messes everything up. Sure, I can make it work but it's pretty pointless when I don't want to change states and instead I have code firing twice I would rather not be.

    I would like my view to react to events without making state changes. If anything, I would rather filter events for certain states. For example, if someone "likes" a picture, you might send a Like event during the ShowingPhoto state. Your library could make the validation and what not.

    In lieu of this functionality, I made transitions with nil => nil

    bug question 
    opened by joslinm 9
  • Support for Hierarchical State Machine?

    Support for Hierarchical State Machine?

    Hi @inamiy, I really like your take on state machines in Swift. I'm now running into an issue in which support for a Hierarchical State Machine would benefit the architectural design. For example, I have three states: A, B and C. A and B are two variants; and it is possible to go from either A or B to C. However, from C it is only possible to go to A. So I would need a handler (A, B) => C and C => A. However SwiftState doesn't allow me to create a handler (A, B) => C, so either I duplicate the handler (no good), or disregard the current state and add nil => C (but that also allows C => C, and a future D => C). As the states grow complexer, this would result in headaches; as adding yet another state would require re-evaluating all the current handlers.

    opened by Bouke 9
  • Machine.addHandler(transition: order: handler:) -> Disposable not working ?

    Machine.addHandler(transition: order: handler:) -> Disposable not working ?

    I modified the basic test case as following and it's failing. Any ideas to get rid of ?

        func testREADME_tryEvent()
        {
            var handlerCalled = false
            let machine = StateMachine<MyState, MyEvent>(state: .State0) { machine in
    
                // add 0 => 1 => 2
                machine.addRoutes(event: .Event0, transitions: [
                    .State0 => .State1,
                    .State1 => .State2,
                ])
                machine.addHandler(.Any => .Any) { _ in
                    handlerCalled = true
                }
            }
    
            // 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")
    
            XCTAssertTrue(handlerCalled) // Failure
        }
    
    opened by toshi0383 6
  • Can't build with Swift 2.0 on XCode7 beta5

    Can't build with Swift 2.0 on XCode7 beta5

    When I try to build the swift/2.0 branch on XCode7, I'm getting nearly a hundred errors about private class names. I think what's happening is that the private type aliases are being misinterpreted by the swift compiler. It's difficult to test this assumption because I'm getting so many build errors that I can't find the root of the type/alias dependencies. Have you seen anything like this?

    opened by dcunited001 5
  • Swift 4.0 support

    Swift 4.0 support

    Hi there! The current swift/3.0 branch doesn't work on projects that target Swift 4.0. This PR is a result of running Xcode 9 migrators, cleaning up new warnings, and fixing making one value public instead of internal.

    There's no swift/4.0 branch yet so I had to target swift/3.0. If you create a 4.0 branch I'll change the PR's target.

    opened by adamyanalunas 4
  • iOS 7 support

    iOS 7 support

    I've got a question regarding iOS support.

    Do you know if SwiftState will work if I include the code in an iOS project that has min deployment target of iOS 7?

    Thanks

    opened by bogosmer 4
  • Add `addAnyHandler()`.

    Add `addAnyHandler()`.

    This pull request is a new feature for #40 to support addAnyHandler() which works for both event-based & state-based transitions.

    See test case: StateMachineEventTests.swift#L382-L446

    enhancement 
    opened by inamiy 4
  • onEntry and onExit handlers?

    onEntry and onExit handlers?

    From the README, I can see that you could construct state entry and exit handlers likes so:

    // onExit handler
    machine.addHandler(.State0 => .Any) { context in
       print("0 => 1")
    }
    
    // onEntry handler
    machine.addHandler(.Any => .State0) { context in
       print("0 => 1")
    }
    

    But will these handlers be executed in the expected order? I don't really want to have to manage order through the int myself...

    opened by samskiter 3
  • Introduce fastlane.

    Introduce fastlane.

    This pull request will introduce fastlane as easy command-line tool.

    I also added test_universal_framework.rb as "fastlane-action" to explicitly specify destination (e.g. iphonesimulator) for single-target universal framework.

    opened by inamiy 3
  • State & Event enums with associated values complete example

    State & Event enums with associated values complete example

    Hi

    I'm having some trouble understanding the 'State & Event enums with associated values' example on the main project page. I'm specifically confused by incomplete enum definitions ...

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

    Can someone please respond to this question with a complete example or update the project README ?

    Thanks Dave

    opened by dekhaus 0
  • Would be really helpful if you version it better with each commit to master or swift/4.0

    Would be really helpful if you version it better with each commit to master or swift/4.0

    Apologies for creating an issue for this but it's hectic to keep track of your releases. Would be really helpful if you can start versioning it in a better way. Thanks. :)

    opened by rameswarprasad 0
  • Carthage Build failed with exit code 65

    Carthage Build failed with exit code 65

    On carhage update, build fail:

    carthage version: 0.27.0 xcode version: 9.2

    Build Failed Task failed with exit code 65: /usr/bin/xcrun xcodebuild -project /Users/vml933/Documents/XCodeProject/PalLinkTest3/Carthage/Checkouts/SwiftState/SwiftState.xcodeproj -scheme SwiftState -configuration Release -derivedDataPath /Users/vml933/Library/Caches/org.carthage.CarthageKit/DerivedData/9.2_9C40b/SwiftState/4.1.0 -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES archive -archivePath /var/folders/c6/cmhy9djd5y52994tjg3lsq7c0000gn/T/SwiftState SKIP_INSTALL=YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO CLANG_ENABLE_CODE_COVERAGE=NO (launched in /Users/vml933/Documents/XCodeProject/PalLinkTest3/Carthage/Checkouts/SwiftState)

    This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/c6/cmhy9djd5y52994tjg3lsq7c0000gn/T/carthage-xcodebuild.WbBQpz.log

    opened by vml933 8
  • suggestion about state and context

    suggestion about state and context

    Suggestions and comments about the design (not a bug.)

    Should state be an attribute of the machine, or the object which is going through the machine?

    My use case is a game having many tokens (AKA sprites) each having state and a state machine. I don't mind each token owning an instance of StateMachine, but then I must configure each instance the same way (easy enough to workaround.) But possibly a StateMachine should have a Design which is configured and passed to a StateMachine instance.

    In other implementations of FSM 'context' seems to mean: the object which has state, which the state machine sets the state of, and which is passed to all actions (what you call Handlers) for them to act on. Your context is not the same thing? Maybe a solution is a delegate for the state machine?

    On another topic, I don't understand the use case for AnyState. Is one use case adding transitions from any state to a reset state, so that you don't have to specify a transition from every state to the reset state? Maybe you could explain in your document. But I should read the code, and for example study what a transition from AnyState to AnyState would mean.

    Thanks, SwiftState is rather elegant.

    enhancement question 
    opened by bootchk 4
Releases(6.0.0)
Owner
ReactKit
Swift Reactive Programming Toolkit
ReactKit
🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.

NOTE: This repository has been discontinued in favor of Actomaton. ?? Harvest Apple's Combine.framework (from iOS 13) + State Machine, inspired by Elm

Yasuhiro Inami 386 Dec 18, 2022
Redux for Swift - a predictable state container for Swift apps

Merge / deprecation announcement: ReduxKit and Swift-Flow have joined forces! The result is ReSwift. The nitty gritty: We decided to deprecate ReduxKi

null 613 Jan 3, 2023
Predictable state container for Swift too

ReduxSwift ReduxSwift is a minimal Swift port of Redux, a popular JavaScript library for application state management. Functionality Centralized State

Lucas Sunsi Abreu 38 Oct 6, 2020
Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux

Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux Introduction VueFlux is the architecture to manage state with unidi

Ryo Aoyama 324 Dec 17, 2022
MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner

CZInstagram MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner. Unidirectional Da

Cheng Zhang 56 Nov 1, 2022
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

RxSwift Community 125 Jan 29, 2022
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.

Verge.swift ?? An effective state management architecture for iOS - UIKit and also SwiftUI ?? _ An easier way to get unidirectional data flow _ _ Supp

VergeGroup 478 Dec 29, 2022
CMPSC475 Final Project, ArboretumID Application allows users to explore the Penn State Arboretum, identify plants and learn about the exhibits!

ArboretumID: CMPSC475 Final Project Taylan Unal (@taylanu) About ArboretumID ArboretumIdentifier (ArboretumID) is an app that enhances the Penn State

Taylan 1 Nov 27, 2021
An experimental time traveling state store for SwiftUI

SwiftUI Time Travel A SwiftUI state store and view that allow you to scrub through an application's state. This is a super rough prototype: it's only

Tim Donnelly 139 Sep 14, 2022
A super simple library for state management with unidirectional data flow.

OneWay ?? OneWay is still experimental. As such, expect things to break and change in the coming months. OneWay is a super simple library for state ma

SeungYeop Yeom 41 Dec 20, 2022
RxSwift wrapper around the elegant HTTP networking in Swift Alamofire

RxAlamofire RxAlamofire is a RxSwift wrapper around the elegant HTTP networking in Swift Alamofire. Getting Started Wrapping RxSwift around Alamofire

RxSwift Community 1.6k Jan 3, 2023
Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features.

swift-unidirectional-flow Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features. struct SearchState: Equatabl

Majid Jabrayilov 104 Dec 26, 2022
Reactive Programming in Swift

Rx is a generic abstraction of computation expressed through Observable<Element> interface, which lets you broadcast and subscribe to values and other

ReactiveX 23.1k Jan 5, 2023
RxSwift extentions for Swift optionals and "Occupiable" types

RxOptional RxSwift extentions for Swift optionals and "Occupiable" types. Usage All operators are available on Driver as well unless otherwise marked.

Thane Gill 8 Jun 28, 2020
Unidirectional Data Flow in Swift - Inspired by Redux

ReSwift Supported Swift Versions: Swift 4.2, 5.x For Swift 3.2 or 4.0 Support use Release 5.0.0 or earlier. For Swift 2.2 Support use Release 2.0.0 or

null 7.3k Dec 25, 2022
A Swift Reactive Programming Kit

ReactiveKit is a lightweight Swift framework for reactive and functional reactive programming that enables you to get into the reactive world today. T

Declarative Hub 1.2k Dec 29, 2022
An array class implemented in Swift that can be observed using ReactiveCocoa's Signals

ReactiveArray An array class implemented in Swift that can be observed using ReactiveCocoa's Signals. Installation Carthage Add the following to your

Wolox 53 Jan 29, 2022
Simple and lightweight Functional Reactive Coding in Swift for the rest of us

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find. This library does not use the term FRP (Functional R

Jens Ravens 1.1k Jan 3, 2023
Aftermath is a stateless message-driven micro-framework in Swift

Aftermath is a stateless message-driven micro-framework in Swift, which is based on the concept of the unidirectional data flow architecture.

HyperRedink 70 Dec 24, 2021