Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/inamiy/Harvest
đŸ Harvest: Apple's Combine.framework + State Machine, inspired by Elm.
https://github.com/inamiy/Harvest
automaton combine-framework elm redux state-machine swift
Last synced: 3 months ago
JSON representation
đŸ Harvest: Apple's Combine.framework + State Machine, inspired by Elm.
- Host: GitHub
- URL: https://github.com/inamiy/Harvest
- Owner: inamiy
- License: mit
- Archived: true
- Created: 2019-06-07T00:51:39.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2021-12-12T05:04:14.000Z (almost 3 years ago)
- Last Synced: 2024-05-22T15:32:53.375Z (6 months ago)
- Topics: automaton, combine-framework, elm, redux, state-machine, swift
- Language: Swift
- Homepage:
- Size: 298 KB
- Stars: 384
- Watchers: 4
- Forks: 11
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-about-SwiftUI - Harvest
README
## NOTE: This repository has been discontinued in favor of [Actomaton](https://github.com/inamiy/Actomaton).
# đŸ Harvest
[![Swift 5.1](https://img.shields.io/badge/swift-5.1-orange.svg?style=flat)](https://swift.org/download/)
![Build Status](https://github.com/inamiy/Harvest/workflows/CI/badge.svg)Apple's [Combine.framework](https://developer.apple.com/documentation/combine) (from iOS 13) + State Machine, inspired by [Elm](http://elm-lang.org/).
This is a sister library of the following projects:
- [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (using [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift))
- [RxAutomaton](https://github.com/inamiy/RxAutomaton) (using [RxSwift](https://github.com/ReactiveX/RxSwift))## Requirement
Xcode 11 (Swift 5.1 / macOS 10.15, iOS 13, ...)
## Example
![](Assets/login-diagram.png)
To make a state transition diagram like above _with additional effects_, follow these steps:
### 1. Define `State`s and `Input`s
```swift
// 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`
```swift
enum EffectQueue: EffectQueueProtocol {
case `default`
case requestvar 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)
```swift
// NOTE: `EffectID` is useful for manual effect cancellation, but not used in this example.
typealias EffectID = Nevertypealias Harvester = Harvest.Harvester
typealias EffectMapping = Harvester.EffectMapping
typealias Effect = Harvester.Effect// 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)
```swift
// Prepare input pipe for sending `Input` to `Harvester`.
let inputs = PassthroughSubject()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!
```swift
let send = inputs.sendexpect(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:
- [NoTests/RxFeedback](https://github.com/NoTests/RxFeedback.swift)
- [Babylonpartners/ReactiveFeedback](https://github.com/Babylonpartners/ReactiveFeedback)
- [sergdort/CombineFeedback](https://github.com/sergdort/CombineFeedback)See [inamiy/ReactiveAutomaton#12](https://github.com/inamiy/ReactiveAutomaton/pull/12) for more discussion.
## Composable Architecture with SwiftUI
[Pull Request \#8](https://github.com/inamiy/Harvest/pull/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](https://github.com/inamiy/FunOptics)See [Harvest-SwiftUI-Gallery](https://github.com/inamiy/Harvest-SwiftUI-Gallery) for the examples.
See also [Babylonpartners/ios-playbook#171](https://github.com/Babylonpartners/ios-playbook/pull/171) for further related discussion.
- [ ] TODO: Write documentation
## References
1. [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (using [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift))
1. [RxAutomaton](https://github.com/inamiy/RxAutomaton) (using [RxSwift](https://github.com/ReactiveX/RxSwift))
1. [iOSDC 2016 (Tokyo, in Japanese)](https://iosdc.jp/2016/) (2016/08/20)
- [iOSDC Japan 2016 08/20 Track A / Reactive State Machine / çšČèŠ æł°ćź - YouTube](https://www.youtube.com/watch?v=Yvz9H9AWGFM) (video)
- [Reactive State Machine (Japanese) // Speaker Deck](https://speakerdeck.com/inamiy/reactive-state-machine-japanese) (slide)
1. [iOSConf SG (Singapore, in English)](http://iosconf.sg/) (2016/10/20-21)
- [Reactive State Machine - iOS Conf SG 2016 - YouTube](https://www.youtube.com/watch?v=Oau4JjJP3nA) (video)
- [Reactive State Machine // Speaker Deck](https://speakerdeck.com/inamiy/reactive-state-machine-1) (slide)## License
[MIT](LICENSE)