{"id":13751478,"url":"https://github.com/inamiy/Harvest","last_synced_at":"2025-05-09T18:31:26.822Z","repository":{"id":142083077,"uuid":"190669554","full_name":"inamiy/Harvest","owner":"inamiy","description":"🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.","archived":true,"fork":false,"pushed_at":"2021-12-12T05:04:14.000Z","size":305,"stargazers_count":384,"open_issues_count":3,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-05-22T15:32:53.375Z","etag":null,"topics":["automaton","combine-framework","elm","redux","state-machine","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/inamiy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-06-07T00:51:39.000Z","updated_at":"2023-11-17T08:16:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"cb8a6d92-0af3-4b52-bad4-03ca985bca23","html_url":"https://github.com/inamiy/Harvest","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inamiy%2FHarvest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inamiy%2FHarvest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inamiy%2FHarvest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inamiy%2FHarvest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/inamiy","download_url":"https://codeload.github.com/inamiy/Harvest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253303024,"owners_count":21886873,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automaton","combine-framework","elm","redux","state-machine","swift"],"created_at":"2024-08-03T09:00:46.205Z","updated_at":"2025-05-09T18:31:26.447Z","avatar_url":"https://github.com/inamiy.png","language":"Swift","funding_links":[],"categories":["Uncategorized","Content","🌎 by the community"],"sub_categories":["Uncategorized","Libraries"],"readme":"## NOTE: This repository has been discontinued in favor of [Actomaton](https://github.com/inamiy/Actomaton).\n\n# 🌾 Harvest\n\n[![Swift 5.1](https://img.shields.io/badge/swift-5.1-orange.svg?style=flat)](https://swift.org/download/)\n![Build Status](https://github.com/inamiy/Harvest/workflows/CI/badge.svg)\n\nApple's [Combine.framework](https://developer.apple.com/documentation/combine) (from iOS 13) + State Machine, inspired by [Elm](http://elm-lang.org/).\n\nThis is a sister library of the following projects:\n\n- [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (using [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift))\n- [RxAutomaton](https://github.com/inamiy/RxAutomaton) (using [RxSwift](https://github.com/ReactiveX/RxSwift))\n\n## Requirement\n\nXcode 11 (Swift 5.1 / macOS 10.15, iOS 13, ...)\n\n## Example\n\n![](Assets/login-diagram.png)\n\nTo make a state transition diagram like above _with additional effects_, follow these steps:\n\n### 1. Define `State`s and `Input`s\n\n```swift\n// 1. Define `State`s and `Input`s.\nenum State {\n    case loggedOut, loggingIn, loggedIn, loggingOut\n}\n\nenum Input {\n    case login, loginOK, logout, logoutOK\n    case forceLogout\n}\n```\n\n### 2. Define `EffectQueue`\n\n```swift\nenum EffectQueue: EffectQueueProtocol {\n    case `default`\n    case request\n\n    var flattenStrategy: FlattenStrategy {\n        switch self {\n        case .default: return .merge\n        case .request: return .latest\n        }\n    }\n\n    static var defaultEffectQueue: EffectQueue {\n        .default\n    }\n}\n```\n\n`EffectQueue` allows additional side-effects (`Effect`, a wrapper of `Publisher`) to be scheduled with a specific `FlattenStrategy`, such as `flatMap` (`.merge`), `flatMapLatest` (`.latest`), etc.\nIn above case, we want to automatically cancel previous network requests if occurred multiple times, so we also prepare `case request` queue with `.latest` strategy.\n\n### 3. Create `EffectMapping` (Effect-wise reducer)\n\n```swift\n// NOTE: `EffectID` is useful for manual effect cancellation, but not used in this example.\ntypealias EffectID = Never\n\ntypealias Harvester = Harvest.Harvester\u003cInput, State\u003e\ntypealias EffectMapping = Harvester.EffectMapping\u003cEffectQueue, EffectID\u003e\ntypealias Effect = Harvester.Effect\u003cInput, EffectQueue, EffectID\u003e\n\n// Additional effects while state-transitioning.\nlet loginOKPublisher = /* show UI, setup DB, request APIs, ..., and send `Input.loginOK` */\nlet logoutOKPublisher = /* show UI, clear cache, cancel APIs, ..., and send `Input.logoutOK` */\nlet forceLogoutOKPublisher = /* do something more special, ..., and send `Input.logoutOK` */\n\nlet canForceLogout: (State) -\u003e Bool = [.loggingIn, .loggedIn].contains\n\nlet mappings: [EffectMapping] = [\n\n  /*  Input   |   fromState =\u003e toState     |      Effect       */\n  /* ----------------------------------------------------------*/\n    .login    | .loggedOut  =\u003e .loggingIn  | Effect(loginOKPublisher, queue: .request),\n    .loginOK  | .loggingIn  =\u003e .loggedIn   | .empty,\n    .logout   | .loggedIn   =\u003e .loggingOut | Effect(logoutOKPublisher, queue: .request),\n    .logoutOK | .loggingOut =\u003e .loggedOut  | .empty,\n\n    .forceLogout | canForceLogout =\u003e .loggingOut | Effect(forceLogoutOKPublisher, queue: .request)\n]\n```\n\n`EffectMapping` is Redux's `Reducer` or Elm's `Update` pure function that also returns `Effect` during the state-transition.\nNote that `queue: .request` is specified so that those effects will be handled in the same queue with `.latest` strategy.\nInstead of writing it as a plain function with pattern-matching, you can also write in a fancy markdown-table-like syntax as shown above.\n\n### 4. Setup `Harvester` (state machine)\n\n```swift\n// Prepare input pipe for sending `Input` to `Harvester`.\nlet inputs = PassthroughSubject\u003cInput, Never\u003e()\n\nvar cancellables: [AnyCancellable] = []\n\n// Setup state machine.\nlet harvester = Harvester(\n    state: .loggedOut,\n    input: inputs,\n    mapping: .reduce(.first, mappings),  // combine mappings using `reduce` helper\n    scheduler: DispatchQueue.main\n)\n\n// Observe state-transition replies (`.success` or `.failure`).\nharvester.replies.sink { reply in\n    print(\"received reply = \\(reply)\")\n}.store(in: \u0026cancellables)\n\n// Observe current state changes.\nharvester.state.sink { state in\n    print(\"current state = \\(state)\")\n}.store(in: \u0026cancellables)\n```\n\nNOTE: `func reduce` is declared to combine multiple `mappings` into one.\n\n### 5. And let's test!\n\n```swift\nlet send = inputs.send\n\nexpect(harvester.state) == .loggedIn    // already logged in\nsend(Input.logout)\nexpect(harvester.state) == .loggingOut  // logging out...\n// `logoutOKPublisher` will automatically send `Input.logoutOK` later\n// and transit to `State.loggedOut`.\n\nexpect(harvester.state) == .loggedOut   // already logged out\nsend(Input.login)\nexpect(harvester.state) == .loggingIn   // logging in...\n// `loginOKPublisher` will automatically send `Input.loginOK` later\n// and transit to `State.loggedIn`.\n\n// 👨🏽 \u003c But wait, there's more!\n// Let's send `Input.forceLogout` immediately after `State.loggingIn`.\n\nsend(Input.forceLogout)                       // 💥💣💥\nexpect(harvester.state) == .loggingOut  // logging out...\n// `forceLogoutOKublisher` will automatically send `Input.logoutOK` later\n// and transit to `State.loggedOut`.\n```\n\nPlease notice how state-transitions, effect calls and cancellation are nicely performed.\nIf your cancellation strategy is more complex than just using `FlattenStrategy.latest`, you can also use `Effect.cancel` to manually stop specific `EffectID`.\n\nNote 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).\n\n## Using `Feedback` effect model as alternative\n\nInstead of using `EffectMapping` with fine-grained `EffectQueue` model, Harvest also supports `Feedback` system as described in the following libraries:\n\n- [NoTests/RxFeedback](https://github.com/NoTests/RxFeedback.swift)\n- [Babylonpartners/ReactiveFeedback](https://github.com/Babylonpartners/ReactiveFeedback)\n- [sergdort/CombineFeedback](https://github.com/sergdort/CombineFeedback)\n\nSee [inamiy/ReactiveAutomaton#12](https://github.com/inamiy/ReactiveAutomaton/pull/12) for more discussion.\n\n## Composable Architecture with SwiftUI\n\n[Pull Request \\#8](https://github.com/inamiy/Harvest/pull/8) introduced `HarvestStore` and `HarvestOptics` frameworks for Composable Architecture, especially focused on SwiftUI.\n\n- **HarvestStore**: 2-way bindable `Store` optimized for SwiftUI\n- **HarvestOptics**: Input \u0026 state lifting helpers using [FunOptics](https://github.com/inamiy/FunOptics)\n\nSee [Harvest-SwiftUI-Gallery](https://github.com/inamiy/Harvest-SwiftUI-Gallery) for the examples.\n\nSee also [Babylonpartners/ios-playbook#171](https://github.com/Babylonpartners/ios-playbook/pull/171) for further related discussion.\n\n- [ ] TODO: Write documentation\n\n## References\n\n1. [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (using [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift))\n1. [RxAutomaton](https://github.com/inamiy/RxAutomaton) (using [RxSwift](https://github.com/ReactiveX/RxSwift))\n1. [iOSDC 2016 (Tokyo, in Japanese)](https://iosdc.jp/2016/) (2016/08/20)\n    - [iOSDC Japan 2016 08/20 Track A / Reactive State Machine / 稲見 泰宏 - YouTube](https://www.youtube.com/watch?v=Yvz9H9AWGFM) (video)\n    - [Reactive State Machine (Japanese) // Speaker Deck](https://speakerdeck.com/inamiy/reactive-state-machine-japanese) (slide)\n1. [iOSConf SG (Singapore, in English)](http://iosconf.sg/) (2016/10/20-21)\n    - [Reactive State Machine - iOS Conf SG 2016 - YouTube](https://www.youtube.com/watch?v=Oau4JjJP3nA) (video)\n    - [Reactive State Machine // Speaker Deck](https://speakerdeck.com/inamiy/reactive-state-machine-1) (slide)\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finamiy%2FHarvest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finamiy%2FHarvest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finamiy%2FHarvest/lists"}