Git Product home page Git Product logo

harvest's Introduction

NOTE: This repository has been discontinued in favor of Actomaton.

๐ŸŒพ Harvest

Swift 5.1 Build Status

Apple's Combine.framework (from iOS 13) + State Machine, inspired by Elm.

This is a sister library of the following projects:

Requirement

Xcode 11 (Swift 5.1 / macOS 10.15, iOS 13, ...)

Example

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

1. Define States and Inputs

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

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

2. Define EffectQueue

enum EffectQueue: EffectQueueProtocol {
    case `default`
    case request

    var flattenStrategy: FlattenStrategy {
        switch self {
        case .default: return .merge
        case .request: return .latest
        }
    }

    static var defaultEffectQueue: EffectQueue {
        .default
    }
}

EffectQueue allows additional side-effects (Effect, a wrapper of Publisher) to be scheduled with a specific FlattenStrategy, such as flatMap (.merge), flatMapLatest (.latest), etc. In above case, we want to automatically cancel previous network requests if occurred multiple times, so we also prepare case request queue with .latest strategy.

3. Create EffectMapping (Effect-wise reducer)

// NOTE: `EffectID` is useful for manual effect cancellation, but not used in this example.
typealias EffectID = Never

typealias Harvester = Harvest.Harvester<Input, State>
typealias EffectMapping = Harvester.EffectMapping<EffectQueue, EffectID>
typealias Effect = Harvester.Effect<Input, EffectQueue, EffectID>

// Additional effects while state-transitioning.
let loginOKPublisher = /* show UI, setup DB, request APIs, ..., and send `Input.loginOK` */
let logoutOKPublisher = /* show UI, clear cache, cancel APIs, ..., and send `Input.logoutOK` */
let forceLogoutOKPublisher = /* do something more special, ..., and send `Input.logoutOK` */

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

let mappings: [EffectMapping] = [

  /*  Input   |   fromState => toState     |      Effect       */
  /* ----------------------------------------------------------*/
    .login    | .loggedOut  => .loggingIn  | Effect(loginOKPublisher, queue: .request),
    .loginOK  | .loggingIn  => .loggedIn   | .empty,
    .logout   | .loggedIn   => .loggingOut | Effect(logoutOKPublisher, queue: .request),
    .logoutOK | .loggingOut => .loggedOut  | .empty,

    .forceLogout | canForceLogout => .loggingOut | Effect(forceLogoutOKPublisher, queue: .request)
]

EffectMapping is Redux's Reducer or Elm's Update pure function that also returns Effect during the state-transition. Note that queue: .request is specified so that those effects will be handled in the same queue with .latest strategy. Instead of writing it as a plain function with pattern-matching, you can also write in a fancy markdown-table-like syntax as shown above.

4. Setup Harvester (state machine)

// Prepare input pipe for sending `Input` to `Harvester`.
let inputs = PassthroughSubject<Input, Never>()

var cancellables: [AnyCancellable] = []

// Setup state machine.
let harvester = Harvester(
    state: .loggedOut,
    input: inputs,
    mapping: .reduce(.first, mappings),  // combine mappings using `reduce` helper
    scheduler: DispatchQueue.main
)

// Observe state-transition replies (`.success` or `.failure`).
harvester.replies.sink { reply in
    print("received reply = \(reply)")
}.store(in: &cancellables)

// Observe current state changes.
harvester.state.sink { state in
    print("current state = \(state)")
}.store(in: &cancellables)

NOTE: func reduce is declared to combine multiple mappings into one.

5. And let's test!

let send = inputs.send

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

expect(harvester.state) == .loggedOut   // already logged out
send(Input.login)
expect(harvester.state) == .loggingIn   // logging in...
// `loginOKPublisher` 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(harvester.state) == .loggingOut  // logging out...
// `forceLogoutOKublisher` will automatically send `Input.logoutOK` later
// and transit to `State.loggedOut`.

Please notice how state-transitions, effect calls and cancellation are nicely performed. If your cancellation strategy is more complex than just using FlattenStrategy.latest, you can also use Effect.cancel to manually stop specific EffectID.

Note that any sizes of State and Input will work using Harvester, from single state (like above example) to covering whole app's states (like React.js + Redux architecture).

Using Feedback effect model as alternative

Instead of using EffectMapping with fine-grained EffectQueue model, Harvest also supports Feedback system as described in the following libraries:

See inamiy/ReactiveAutomaton#12 for more discussion.

Composable Architecture with SwiftUI

Pull Request #8 introduced HarvestStore and HarvestOptics frameworks for Composable Architecture, especially focused on SwiftUI.

  • HarvestStore: 2-way bindable Store optimized for SwiftUI
  • HarvestOptics: Input & state lifting helpers using FunOptics

See Harvest-SwiftUI-Gallery for the examples.

See also Babylonpartners/ios-playbook#171 for further related discussion.

  • TODO: Write documentation

References

  1. ReactiveAutomaton (using ReactiveSwift)
  2. RxAutomaton (using RxSwift)
  3. iOSDC 2016 (Tokyo, in Japanese) (2016/08/20)
  4. iOSConf SG (Singapore, in English) (2016/10/20-21)

License

MIT

harvest's People

Contributors

inamiy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

harvest's Issues

README contains broken code

Sample code in README causes compile error.
I test with Harvest v0.3.0 (Xcode 13.1) using new blank mac app.

// compile error: Generic type 'EffectMapping' specialized with too few type parameters (got 2, but expected 3)
typealias EffectMapping = Harvester.EffectMapping<EffectQueue, EffectID>
// 'Effect' is not a member type of generic class 'MyApp.Harvester' (aka 'Harvester<Input, State>')
typealias Effect = Harvester.Effect<Input, EffectQueue, EffectID>

Full source code:

import Harvest
import Combine

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

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

enum EffectQueue: EffectQueueProtocol {
    case `default`
    case request

    var flattenStrategy: FlattenStrategy {
        switch self {
        case .default: return .merge
        case .request: return .latest
        }
    }

    static var defaultEffectQueue: EffectQueue {
        .default
    }
}

// NOTE: `EffectID` is useful for manual effect cancellation, but not used in this example.
typealias EffectID = Never

typealias Harvester = Harvest.Harvester<Input, State>
typealias EffectMapping = Harvester.EffectMapping<EffectQueue, EffectID>
typealias Effect = Harvester.Effect<Input, EffectQueue, EffectID>

// Additional effects while state-transitioning.
let loginOKPublisher = Just(()) /* show UI, setup DB, request APIs, ..., and send `Input.loginOK` */
let logoutOKPublisher = Just(()) /* show UI, clear cache, cancel APIs, ..., and send `Input.logoutOK` */
let forceLogoutOKPublisher = Just(()) /* do something more special, ..., and send `Input.logoutOK` */

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

let mappings: [EffectMapping] = [

  /*  Input   |   fromState => toState     |      Effect       */
  /* ----------------------------------------------------------*/
    .login    | .loggedOut  => .loggingIn  | Effect(loginOKPublisher, queue: .request),
    .loginOK  | .loggingIn  => .loggedIn   | .empty,
    .logout   | .loggedIn   => .loggingOut | Effect(logoutOKPublisher, queue: .request),
    .logoutOK | .loggingOut => .loggedOut  | .empty,

    .forceLogout | canForceLogout => .loggingOut | Effect(forceLogoutOKPublisher, queue: .request)
]

https://github.com/inamiy/Harvest/tree/0.3.0#3-create-effectmapping-effect-wise-reducer

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.