πŸ€– RxSwift + State Machine, inspired by Redux and Elm.

Overview

RxAutomaton

RxSwift port of ReactiveAutomaton (State Machine).

Terminology

Whenever the word "signal" or "(signal) producer" appears (derived from ReactiveCocoa), they mean "hot-observable" and "cold-observable".

Example

(Demo app is bundled in the project)

To make a state transition diagram like above with additional effects, follow these steps:

// 1. Define `State`s and `Input`s.
enum State {
    case loggedOut, loggingIn, loggedIn, loggingOut
}

enum Input {
    case login, loginOK, logout, logoutOK
    case forceLogout
}

// Additional effects (`Observable`s) while state-transitioning.
// (NOTE: Use `Observable.empty()` for no effect)
let loginOKProducer = /* show UI, setup DB, request APIs, ..., and send `Input.loginOK` */
let logoutOKProducer = /* show UI, clear cache, cancel APIs, ..., and send `Input.logoutOK` */
let forcelogoutOKProducer = /* do something more special, ..., and send `Input.logoutOK` */

let canForceLogout: State -> Bool = [.loggingIn, .loggedIn].contains

// 2. Setup state-transition mappings.
let mappings: [Automaton<State, Input>.EffectMapping] = [

  /*  Input      |   fromState => toState        |      Effect       */
  /* ----------------------------------------------------------------*/
    .login       | .loggedOut  => .loggingIn     | loginOKProducer,
    .loginOK     | .loggingIn  => .loggedIn      | .empty(),
    .logout      | .loggedIn   => .loggingOut    | logoutOKProducer,
    .logoutOK    | .loggingOut => .loggedOut     | .empty(),
    .forceLogout | canForceLogout => .loggingOut | forceLogoutOKProducer
]

// 3. Prepare input pipe for sending `Input` to `Automaton`.
let (inputSignal, inputObserver) = Observable<Input>.pipe()

// 4. Setup `Automaton`.
let automaton = Automaton(
    state: .loggedOut,
    input: inputSignal,
    mapping: reduce(mappings),  // combine mappings using `reduce` helper
    strategy: .latest   // NOTE: `.latest` cancels previous running effect
)

// Observe state-transition replies (`.success` or `.failure`).
automaton.replies.subscribe(next: { reply in
    print("received reply = \(reply)")
})

// Observe current state changes.
automaton.state.asObservable().subscribe(next: { state in
    print("current state = \(state)")
})

And let's test!

let send = inputObserver.onNext

expect(automaton.state.value) == .loggedIn    // already logged in
send(Input.logout)
expect(automaton.state.value) == .loggingOut  // logging out...
// `logoutOKProducer` will automatically send `Input.logoutOK` later
// and transit to `State.loggedOut`.

expect(automaton.state.value) == .loggedOut   // already logged out
send(Input.login)
expect(automaton.state.value) == .loggingIn   // logging in...
// `loginOKProducer` will automatically send `Input.loginOK` later
// and transit to `State.loggedIn`.

// πŸ‘¨πŸ½ < But wait, there's more!
// Let's send `Input.forceLogout` immediately after `State.loggingIn`.

send(Input.forceLogout)                       // πŸ’₯πŸ’£πŸ’₯
expect(automaton.state.value) == .loggingOut  // logging out...
// `forcelogoutOKProducer` will automatically send `Input.logoutOK` later
// and transit to `State.loggedOut`.

License

MIT

Comments
  • Fix double subscription bug

    Fix double subscription bug

    Hello, my team recently start a transition to RxSwfit 4.x, as far as I can see, RxAutomaton does not have rxswift 4 branch and HEAD dependency is ~> 4.0.0. After manual transition(plain dependency bump) we have observed strange behavior: RxAutomaton subscribes to each cold edge twice.

    The bug was caused by updated .share operator, which now have two parameters(replay elements count and strategy) with strange default values: by default new share behavior differs from the old one.

    My PR contains three commits:

    • dependency bump
    • test case, that exposes a bug(https://github.com/inamiy/RxAutomaton/commit/5b2431af90b71593d24b163a164a340d598b5c83)
    • fix (https://github.com/inamiy/RxAutomaton/commit/44703bf93a8950f81db3ec3c15a5e689b0836937)

    So far, this is the only critical issue, caused by the transition to RxSwift 4.x, maybe we should perform needed tests/warning fixes and bump dependency version?

    opened by haritowa 5
  • Reduce specificity for RxSwift podspec dependency

    Reduce specificity for RxSwift podspec dependency

    s.dependency "RxSwift", "~> 3.0.0" is too specific, causing conflicts with packages using newer versions of RxSwift in the 3.x series. Use ~> 3.0 instead.

    I have updated Carthage dependencies locally, built, and run tests (all passed), but did not include the Carthage updates on this PR. Those are easy to add, if useful. Tested with Carthage versions:

    github "Quick/Nimble" "v6.1.0"
    github "Quick/Quick" "v1.1.0"
    github "ReactiveX/RxSwift" "3.4.0"
    github "mrackwitz/xcconfigs" "3.0"
    github "shu223/Pulsator" "0.3.0"
    
    opened by jwhitley 3
  • Producer being called multiple times for one input

    Producer being called multiple times for one input

    @inamiy I'm running into an interesting problem. I've noticed that on every state change, my observable is subscribed to twice, executing the code inside the observable twice.

    To illustrate the problem, I forked the demo, and added some logs: https://github.com/AndrewSB/RxAutomaton/blob/swift/3.0/Demo/ViewController.swift#L42

    Let me know if you need anything else to repro the issue I'm having πŸ‘

    Not sure why it's happening currently - maybe somewhere inside the source the producer is subscribed to twice?

    bug 
    opened by AndrewSB 3
  • Cut a 0.2 release: Swift 3 & RxSwift 3

    Cut a 0.2 release: Swift 3 & RxSwift 3

    Hey @inamiy!

    I was thinking we could cut a new release so I can stop targeting a branch. I tried creating one, but I don't think it gives me the option to create one. Can you try?

    opened by AndrewSB 1
  • Simplify code

    Simplify code

    This PR applies the same code improvements as already done in:

    • https://github.com/inamiy/ReactiveAutomaton/pull/10
    • https://github.com/inamiy/ReactiveAutomaton/pull/11

    by removing the redundant state caching and mapping call.

    opened by inamiy 0
  • Support Swift Package Manager with PackageDescriptionV4

    Support Swift Package Manager with PackageDescriptionV4

    This PR applies the same fix in https://github.com/inamiy/ReactiveAutomaton/pull/8 and https://github.com/inamiy/ReactiveAutomaton/pull/9 , plus Linux support.

    opened by inamiy 0
  • Update dependencies & disable SPM build on Linux

    Update dependencies & disable SPM build on Linux

    This PR updates dependency versions as follows, including Swift 3.0 to 3.1 (Xcode 8.3):

    -github "Quick/Nimble" "v5.1.0"
    -github "shu223/Pulsator" "0.2.0"
    -github "Quick/Quick" "v0.10.0"
    -github "ReactiveX/RxSwift" "3.0.0"
    +github "Quick/Nimble" "v6.1.0"
    +github "shu223/Pulsator" "0.3.0"
    +github "Quick/Quick" "v1.1.0"
    +github "ReactiveX/RxSwift" "3.4.0"
    

    Unfortunately, Linux test didn't work well since #12, so I decided to comment-out it for a while (Help wanted!)

    opened by inamiy 0
  • Add state-change-function support for syntax-sugar mapping

    Add state-change-function support for syntax-sugar mapping

    Ref: https://github.com/inamiy/ReactiveAutomaton/pull/5

    This PR will support state-changing function as follows:

    let mappings: [Automaton.NextMapping] = [
      /*   Input   |    State   |  Effect   */
      /* -----------------------------------*/
        .increment | { $0 + 1 } | .empty,
        .decrement | { $0 - 1 } | .empty,
    ]
    
    opened by inamiy 0
  • Pass input parameters to producer in mappings

    Pass input parameters to producer in mappings

    Along the lines of #1, I'd like to be able to pass the parameters from the input into the producer. For example, if I have a producer: discoverCharacteristicsProducer: ([CBService]) -> Observable<Event>, I know I can call it by using a closure as my NextMapping screen shot 2016-09-26 at 5 06 40 pm

    in the last element ^ I capture the services from the .discoveredServices event, and I then pass it into the discoverCharacteristicsProducer

    Is it possible to do capture the param using the pipe mapping syntax? I'm sure theres some way to compose the operation, I'm not able to think of the type signature for a new | operator that would do this

    opened by AndrewSB 8
  • Input with parameters?

    Input with parameters?

    Hey @inamiy! I'm testing out RxAutomaton in favor of SwiftState for a state machine that models the CoreBluetooth's CBCentralManager.

    The issue I'm running into is handling an input with parameters. For example, my "discovered bluetooth peripheral" input takes an argument rssi: Int (case discovered(rssi: Int)). (Here are all of my inputs): screen shot 2016-09-26 at 4 23 21 pm

    If I try to compose my mappings, I get an error saying expects argument of type (rssi: Int): screen shot 2016-09-26 at 4 21 10 pm

    Which I can fulfill by explicitly specifying some integer amount, but I'd like to be allow the mapping to take effect for every .discovered input, regardless of integer amount. Is that currently possible with RxAutomaton?

    opened by AndrewSB 0
Releases(0.2.1)
Owner
Yasuhiro Inami
Functional Programmer at @delyjp / KURASHIRU / クラシル. Interests: Swift / Haskell / PureScript / Elm / Rust / TypeScript / Category Theory
Yasuhiro Inami
Elm-parcel-capacitor - A sample setup to build an app with Elm, Capacitor, Parcel and Tailwind CSS

Elm, Capacitor, Parcel and Tailwindcss This project is a sample setup to build a

Anthonny QuΓ©rouil 10 May 9, 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
Elegant state machine for Swift.

SwiftState Elegant state machine for Swift. Example enum MyState: StateType { case state0, state1, state2 } // setup state machine let machine = S

ReactKit 885 Dec 16, 2022
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
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 Jan 3, 2023
Realm RxSwift - This application was written in order to use Realm, RxSwift frameworks in real example

Realm_RxSwift This simple app was written to introduce basic operations of some

Elbek Khasanov 3 Apr 7, 2022
A lightweight Elm-like Store for SwiftUI

ObservableStore A simple Elm-like Store for SwiftUI, based on ObservableObject. ObservableStore helps you craft more reliable apps by centralizing all

Subconscious 28 Nov 8, 2022
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
Sample iOS application in SwiftUI presenting Redux architecture

SwiftUI-Redux-Demo Sample iOS application in SwiftUI presenting Redux architecture. My full article about Redux in detail you will find here: Redux ar

Wojciech Kulik 25 Nov 27, 2022
πŸ’Ž Redux like architecture for SwiftUI

Simple Architecture like Redux Installation SPM dependencies: [ .package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0

Pavel 38 Dec 13, 2022
A barebone, thread-safe Redux-like container for Swift.

SwiftTinyRedux SwiftTinyRedux is a barebone, thread-safe Redux-like container for Swift. It features a minimal API and supports composable reducers. I

Valentin Radu 9 Nov 13, 2022
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
STDevRxExt contains some extension functions for RxSwift and RxCocoa which makes our live easy.

STDevRxExt Example To run the Example.playground, clone the repo, and run pod install from the Example directory first. Requirements iOS 9.0+ tvOS 9.0

STDev 6 Mar 26, 2021
RxAlamoRecord combines the power of the AlamoRecord and RxSwift libraries to create a networking layer that makes interacting with API's easier than ever reactively.

Written in Swift 5 RxAlamoRecord combines the power of the AlamoRecord and RxSwift libraries to create a networking layer that makes interacting with

Dalton Hinterscher 9 Aug 7, 2020
🟣 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
Super Simple Pager with RxSwift extension

SSPager Super Simple Pager Example To run the example project, clone the repo, and run pod install from the SSPagerExample directory first. Installati

9oya 4 Jul 10, 2022
RxSwift bindings for Permissions API in iOS.

RxPermission RxSwift bindings for Permission API that helps you with Permissions in iOS. Installation RxPermission is available through CocoaPods. I c

Luke 230 Dec 27, 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