/RxFeedback

Architecture for RxSwift

Primary LanguageSwiftMIT LicenseMIT

RxFeedback

Travis CI platforms pod Carthage compatible Swift Package Manager compatible

The simplest architecture for RxSwift

/**
     Simulation of a discrete system (finite-state machine) with feedback loops.
     Interpretations:
     - [system with feedback loops](https://en.wikipedia.org/wiki/Control_theory)
     - [fixpoint solver](https://en.wikipedia.org/wiki/Fixed_point)
     - [local equilibrium point calculator](https://en.wikipedia.org/wiki/Mechanical_equilibrium)
     - ....

     System simulation will be started upon subscription and stopped after subscription is disposed.

     System state is represented as a `State` parameter.
     Events are represented by `Event` parameter.

     - parameter initialState: Initial state of the system.
     - parameter accumulator: Calculates new system state from existing state and a transition event (system integrator, reducer).
     - parameter feedback: Feedback loops that produce events depending on current system state.
     - returns: Current state of the system.
     */
    public static func system<State, Event>(
            initialState: State,
            reduce: @escaping (State, Event) -> State,
            feedback: (Observable<State>) -> Observable<Event>...
        ) -> Observable<State>

Why

  • Simple

    • If the system doesn't have state -> congrats, you have either a pure function or an observable sequence
    • It the system does have state, here we are :)
    • Interaction with that state is by definition a feedback loop.
    • => It's just state + CQRS
  • Straightforward

    • if it's state -> State
    • if it's a way to modify state -> Event/Command
    • it it's an effect -> encode it into part of state and then design a feedback loop
  • Declarative

    • System behavior is first declaratively specified and effects begin after subscribe is called => Compile time proof there are no "unhandled states"
  • Debugging is easier

    • A lot of logic is just normal pure function that can be debugged using Xcode debugger, or just printing the commands.
  • Can be applied on any level

    • Entire system
    • application (state is stored inside a database, CoreData, Firebase, Realm)
    • view controller (state is stored inside system operator)
    • inside feedback loop (another system operator inside feedback loop)
  • Works awesome with dependency injection

  • Testing

    • Reducer is a pure function, just call it and assert results
    • In case effects are being tested -> TestScheduler
  • Can model circular dependencies

  • Completely separates business logic from effects (Rx).

    • Business logic can be transpiled between platforms (ShiftJS, C++, J2ObjC)

Examples

Simple UI Feedback loop

Observable.system(
    initialState: 0,
    reduce: { (state, event) -> State in
            switch event {
            case .increment:
                return state + 1
            case .decrement:
                return state - 1
            }
        },
    scheduler: MainScheduler.instance,
    feedback:
        // UI is user feedback
        UI.bind { state in
            ([
                state.map(String.init).bind(to: label.rx.text)
            ], [
                plus.rx.tap.map { Event.increment },
                minus.rx.tap.map { Event.decrement }
            ])
        }
    )

Play Catch

Simple automatic feedback loop.

Observable.system(
    initialState: State.humanHasIt,
    reduce: { (state: State, event: Event) -> State in
        switch event {
            case .throwToMachine:
                return .machineHasIt
            case .throwToHuman:
                return .humanHasIt
            }
        },
    scheduler: MainScheduler.instance,
    feedback:
        // UI is human feedback
        bindUI,
        // NoUI, machine feedback
        react(query: { $0.machinePitching }, effects: { () -> Observable<Event> in
            return Observable<Int>
                .timer(1.0, scheduler: MainScheduler.instance)
                .map { _ in Event.throwToHuman }
        })
)

Paging

Driver.system(
    initialState: State.empty,
    reduce: State.reduce,
    feedback:
        // UI, user feedback
        bindUI,
        // NoUI, automatic feedback
        react(query: { $0.loadNextPage }, effects: { resource in
            return URLSession.shared.loadRepositories(resource: resource)
                .asDriver(onErrorJustReturn: .failure(.offline))
                .map(Event.response)
        })
    )

Run RxFeedback.xcodeproj > Example to find out more.

Difference from other architectures

  • Elm - pretty close, feedback loops for effects instead of Cmd, which effects to perform are encoded into state and querried by feedback loops
  • Redux - kind of like this, but feedback loops instead of middleware
  • Redux-Observable - observables observe state vs. being inside middleware between view and state
  • Cycle.js - no simple explanation :), ask @andrestaltz
  • MVVM - separates state from effects and doesn't require a view