{"id":15288640,"url":"https://github.com/gilbox/cloe","last_synced_at":"2025-04-13T08:09:50.248Z","repository":{"id":56906020,"uuid":"233293216","full_name":"gilbox/Cloe","owner":"gilbox","description":"Cloe is Redux on Combine for SwiftUI with excellent feng shui.","archived":false,"fork":false,"pushed_at":"2020-02-04T07:51:09.000Z","size":158,"stargazers_count":33,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-13T08:09:44.337Z","etag":null,"topics":["combine-framework","redux","swift","swiftui"],"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/gilbox.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-11T20:31:35.000Z","updated_at":"2023-12-04T22:29:00.000Z","dependencies_parsed_at":"2022-08-21T03:50:11.970Z","dependency_job_id":null,"html_url":"https://github.com/gilbox/Cloe","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbox%2FCloe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbox%2FCloe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbox%2FCloe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbox%2FCloe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gilbox","download_url":"https://codeload.github.com/gilbox/Cloe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248681492,"owners_count":21144700,"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":["combine-framework","redux","swift","swiftui"],"created_at":"2024-09-30T15:51:46.648Z","updated_at":"2025-04-13T08:09:50.225Z","avatar_url":"https://github.com/gilbox.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cloe\n\n[![CI Status](http://img.shields.io/travis/gilbox/Cloe.svg?style=flat)](https://travis-ci.org/gilbox/Cloe)\n![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/gilbox/Cloe)\n[![License](https://img.shields.io/github/license/gilbox/Cloe)](LICENSE)\n\n**Cloe is Redux on Combine for SwiftUI with excellent feng shui.**\n\n## Setup your store\n\n```swift\nstruct AppState {\n  var appName = \"Demo App\"\n  var age = 6\n  var names = [\"hank\", \"cloe\", \"spike\", \"joffrey\", \"fido\", \"kahlil\", \"malik\"]\n\n  static let initialValue = AppState()\n}\n\nenum AppAction: Action {\n  case growup\n}\n\ntypealias AppStore = Store\u003cAppReducer\u003e\n```\n\n## Setup your reducer\n\n```swift\nfunc appReducer(state: inout AppState, action: Action) {\n  guard let action = action as? AppAction else { return }\n  switch action {\n  case .growup:\n    state.age += 1\n  }\n}\n```\n    \n## Instantiate your Store\n\n```swift\n// Create a store with the publisher middleware\n// this middleware allows us to use `PublisherAction`\n// later to dispatch an async action.\nlet store = AppStore(\n  reducer: appReducer,\n  state: .initialValue,\n  middlewares: [createPublisherMiddleware()])\n\n// Inject the store with `.environmentObject()`.\n// Alternatively we could inject it with `.environment()`\nlet contentView = ContentView().environmentObject(store)\n\n// later...\n    window.rootViewController = UIHostingController(rootView: contentView)\n```\n\n## (Optionally) add some convenience extensions to the store\n\nThese extensions improve the ergonomics of working with the store. With the built-in\n`dispatch` function we would normally dispatch with `store.dispatch(AppAction.growup)`.\nWith this `dispatch` extension we can do `store.dispatch(.growup)` instead.\n\nThe `subscript` extension allows us to avoid using a closure with SwiftUI views.\nFor example, a button can be implemented with: `Button(\"Grow up\", action: store[.growup])`.\n\n```swift\nextension Store {\n  func dispatch(_ action: AppAction) {\n    dispatch(action as Action)\n  }\n\n  subscript(_ action: AppAction) -\u003e (() -\u003e Void) {\n    { [weak self] in self?.dispatch(action as Action) }\n  }\n}\n```\n\n## Connect your SwiftUI View to your store\n\nThis is an example of injecting state using a state selector. Here were define \nthe state selector inside of the View, but it can be defined anywhere.\n\n```swift\nstruct MyView: View {\n  var index: Int\n\n  // Define your derived state\n  struct MyDerivedState: Equatable {\n    var age: Int\n    var name: String\n  }\n\n  // Inject your store\n  @EnvironmentObject var store: AppStore\n\n  // Connect to the store\n  var body: some View {\n    Connect(store: store, selector: selector, content: body)\n  }\n\n  // Render something using the selected state\n  private func body(_ state: MyDerivedState) -\u003e some View {\n    Text(\"Hello \\(state.name)!\")\n  }\n  \n  // Setup a state selector\n  private func selector(_ state: AppState) -\u003e MyDerivedState {\n    .init(age: state.age, name: state.names[index])\n  }\n}\n```\n\nIf you want to connect to the state of the store without defining a selector,\nuse `ConnectStore` instead. Note that `ConnectStore` does not currently skip \nduplicate states the way that `Connect` does.\n\n## Dispatching a simple action\n\nHere's how you can dispatch a simple action:\n\n```swift\n    Button(\"Grow up\") { self.store.dispatch(AppAction.growup) }\n    \n    // ... or ...\n    \n    Button(\"Grow up\", action: store[AppAction.growup])\n```\n\nOr with the optional [`Store` extension](https://github.com/gilbox/Cloe#optionally-add-some-convenience-extensions-to-the-store) mentioned above:\n    \n```swift\n\n    Button(\"Grow up\") { self.store.dispatch(.growup) }\n\n    // ...or...\n\n    Button(\"Grow up\", action: store[.growup])\n```\n\n## Dispatching an async action with the publisher middleware\n\nBelow is a simple example, read more about publisher middleware [here](./Sources/Cloe/PublisherMiddleware/README.md).\n\n```swift\n\n    Button(\"Grow up\") { self.store.dispatch(self.delayedGrowup) }\n    \n  //...\n\n  private let delayedGrowup = PublisherAction\u003cAppState\u003e { dispatch, getState, cancellables in\n    Just(())\n      .delay(for: 2, scheduler: RunLoop.main)\n      .sink { _ in\n        dispatch(AppAction.growup)\n      }\n      .store(in: \u0026cancellables)\n  }\n```\n\n## Tracking async task progress with publisher dispatcher\n\n[Publisher dispatcher documentation](./Sources/Cloe/PublisherDispatcher/README.md).\n\n## How is it different from ReSwift?\n\n- ReSwift is battle tested.\n- ReSwift is being used in real production apps.\n- Cloe uses [Combine Publishers](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Cloe.swift) instead of a [bespoke StoreSubscriber](https://github.com/ReSwift/ReSwift/blob/master/ReSwift/CoreTypes/StoreSubscriber.swift) \n- Cloe's [Middleware](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Cloe.swift) is simpler than [ReSwift's Middleware](https://github.com/ReSwift/ReSwift/blob/master/ReSwift/CoreTypes/Middleware.swift) but achieves the same level of flexibility.\n- Cloe's [combineMiddleware](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Cloe.swift) function is simpler and easier-to-read.\n- Cloe provides a slick way to connect your SwiftUI views.\n- Cloe does not have a skip-repeats option for the main Store state, but when you [`Connect`](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Connect.swift) it to a SwiftUI component it always skips repeated states (subject to change).\n\n## Why does the `Store` object conform to `ObservableObject`?\n\nYou may have noticed that Cloe's [`Store`](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Cloe.swift) class conforms to [`ObservableObject`](https://developer.apple.com/documentation/combine/observableobject).\nHowever, the `Store` **does not contain any `@Published` properties**. This conformance \nis only added to make it easy to inject your store with [`.environmentObject()`](https://developer.apple.com/documentation/swiftui/environmentobject).\nHowever, since we don't expose any `@Published` vars don't expect a view with\n\n```swift\n@ObservedObject var store: AppStore\n```\n\nto automatically re-render when the store changes. This design is intentional so you can \n[subscribe](https://github.com/gilbox/Cloe#connect-your-swiftui-view-to-your-store) to more granular updates with [`Connect`](https://github.com/gilbox/Cloe/blob/master/Sources/Cloe/Connect.swift).\n\n## Example\n\nTo run the example project, clone this repo, and open iOS Example.xcworkspace from the iOS Example directory.\n\n\n## Requirements\n\n- iOS 13\n- macOS 10.15\n- watchOS 6\n- tvOS 13\n\n## Installation\n\nAdd this to your project using Swift Package Manager. In Xcode that is simply: File \u003e Swift Packages \u003e Add Package Dependency... and you're done. Alternative installations options are shown below for legacy projects.\n\n### CocoaPods\n\nIf you are already using [CocoaPods](http://cocoapods.org), just add 'Cloe' to your `Podfile` then run `pod install`.\n\n### Carthage\n\nIf you are already using [Carthage](https://github.com/Carthage/Carthage), just add to your `Cartfile`:\n\n```ogdl\ngithub \"gilbox/Cloe\" ~\u003e 0.3.0\n```\n\nThen run `carthage update` to build the framework and drag the built `Cloe`.framework into your Xcode project.\n\n\n## License\n\nCloe is available under the MIT license. See [the LICENSE file](LICENSE) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilbox%2Fcloe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgilbox%2Fcloe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilbox%2Fcloe/lists"}