{"id":18573957,"url":"https://github.com/subconsciousnetwork/observablestore","last_synced_at":"2026-03-09T13:05:09.726Z","repository":{"id":42832884,"uuid":"463615131","full_name":"subconsciousnetwork/ObservableStore","owner":"subconsciousnetwork","description":"A lightweight Elm-like Store for SwiftUI","archived":false,"fork":false,"pushed_at":"2024-03-04T16:35:57.000Z","size":144,"stargazers_count":39,"open_issues_count":5,"forks_count":7,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-10T20:56:45.255Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/subconsciousnetwork.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-02-25T17:12:24.000Z","updated_at":"2024-04-18T15:17:11.000Z","dependencies_parsed_at":"2023-12-04T21:26:09.980Z","dependency_job_id":"8d50b496-c56e-4da9-9f85-c7f37833674b","html_url":"https://github.com/subconsciousnetwork/ObservableStore","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/subconsciousnetwork/ObservableStore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subconsciousnetwork%2FObservableStore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subconsciousnetwork%2FObservableStore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subconsciousnetwork%2FObservableStore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subconsciousnetwork%2FObservableStore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/subconsciousnetwork","download_url":"https://codeload.github.com/subconsciousnetwork/ObservableStore/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subconsciousnetwork%2FObservableStore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30297111,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T11:12:22.024Z","status":"ssl_error","status_checked_at":"2026-03-09T11:10:54.577Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-11-06T23:13:29.264Z","updated_at":"2026-03-09T13:05:09.703Z","avatar_url":"https://github.com/subconsciousnetwork.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ObservableStore\n\nA simple Elm-like Store for SwiftUI, based on [ObservableObject](https://developer.apple.com/documentation/combine/observableobject).\n\nObservableStore helps you craft more reliable apps, by centralizing all of your application state into one place and giving you a deterministic system for managing state changes and side-effects. All state updates happen through actions passed to an update function. This guarantees your application will produce exactly the same state, given the same actions in the same order. If you’ve ever used [Elm](https://guide.elm-lang.org/architecture/) or [Redux](https://redux.js.org/), you get the gist.\n\nBecause `Store` is an [ObservableObject](https://developer.apple.com/documentation/combine/observableobject), it can be used anywhere in SwiftUI that ObservableObject would be used.\n\nYou can centralize all application state in a single Store, use the Store as an [`EnvironmentObject`](https://developer.apple.com/documentation/swiftui/environmentobject), or create multiple `@StateObject` stores. You can also pass scoped parts of a store down to sub-views as `@Bindings`, as scoped `ViewStores`, or as ordinary bare properties of `store.state`.\n\n## Example\n\nA minimal example of Store used to increment a count with a button.\n\n```swift\nimport SwiftUI\nimport Combine\nimport ObservableStore\n\n/// Actions\nenum AppAction {\n    case increment\n}\n\n/// Services like API methods go here\nstruct AppEnvironment {\n}\n\n/// Conform your model to `ModelProtocol`.\n/// A `ModelProtocol` is any `Equatable` that has a static update function\n/// like the one below.\nstruct AppModel: ModelProtocol {\n    var count = 0\n\n    /// Update function\n    static func update(\n        state: AppModel,\n        action: AppAction,\n        environment: AppEnvironment\n    ) -\u003e Update\u003cAppModel\u003e {\n        switch action {\n        case .increment:\n            var model = state\n            model.count = model.count + 1\n            return Update(state: model)\n        }\n    }\n}\n\nstruct AppView: View {\n    @StateObject var store = Store(\n        state: AppModel(),\n        environment: AppEnvironment()\n    )\n\n    var body: some View {\n        VStack {\n            Text(\"The count is: \\(store.state.count)\")\n            Button(\n                action: {\n                    // Send `.increment` action to store,\n                    // updating state.\n                    store.send(.increment)\n                },\n                label: {\n                    Text(\"Increment\")\n                }\n            )\n        }\n    }\n}\n```\n\n## State, updates, and actions\n\nA `Store` is a source of truth for application state. It's an [ObservableObject](https://developer.apple.com/documentation/combine/observableobject), so you can use it anywhere in SwiftUI that you would use an ObservableObject—as an [@ObservedObject](https://developer.apple.com/documentation/swiftui/observedobject), a [@StateObject](https://developer.apple.com/documentation/swiftui/stateobject), or [@EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject).\n\nStore exposes a single [`@Published`](https://developer.apple.com/documentation/combine/published) property, `state`, which represents your application state. `state` can be any type that conforms to `ModelProtocol`.\n\n`state` is read-only, and cannot be updated directly. Instead, all state changes are returned by an update function that you implement as part of `ModelProtocol`.\n\n```swift\nstruct AppModel: ModelProtocol {\n    var count = 0\n\n    /// Update function\n    static func update(\n        state: AppModel,\n        action: AppAction,\n        environment: AppEnvironment\n    ) -\u003e Update\u003cAppModel\u003e {\n        switch action {\n        case .increment:\n            var model = state\n            model.count = model.count + 1\n            return Update(state: model)\n        }\n    }\n}\n```\n\nThe `Update` returned is a small struct that contains a new state, plus any optional effects and animations associated with the state transition (more about that in a bit).\n\n`ModelProtocol` inherits from `Equatable`. Before setting a new state, Store checks that it is not equal to the previous state. New states that are equal to old states are not set, making them a no-op. This means views only recalculate when the state actually changes.\n\n## Effects\n\n Updates are also able to produce asynchronous effects via [Combine](https://developer.apple.com/documentation/combine) publishers. This gives you a deterministic way to schedule sync and async side-effects, like HTTP requests or database calls in response to actions.\n \nEffects are modeled as [Combine Publishers](https://developer.apple.com/documentation/combine/publishers), which publish actions and never fail. For convenience, ObservableStore defines a typealias for effect publishers:\n\n```swift\npublic typealias Fx\u003cAction\u003e = AnyPublisher\u003cAction, Never\u003e\n```\n\nYou can produce effects by exposing services or methods on `Environment` that produce Combine publishers.\n\nAnother common approach is to make the environment (or some of its services) [actors](https://developer.apple.com/documentation/swift/actor). This has the advantage of getting work off the main thread.\n\n```swift\nactor Environment {\n    // ...\n    func authenticate(credentials: Credentials) async -\u003e Action {\n        // ...\n    }\n}\n```\n\nYou can then wrap actor method calls in publishers. ObservableStore provides a helpful extension for this that allows you to construct a [Combine Future](https://developer.apple.com/documentation/combine/future) from an async closure.\n\nHere's an example of creating an effect using an environment actor and returning it as part of the update:\n\n```swift\nfunc update(\n    state: Model,\n    action: Action,\n    environment: Environment\n) -\u003e Update\u003cModel\u003e {\n    switch action {\n    // ...\n    case .authenticate(let credentials):\n        let fx = Future {\n            await environment.authenticate(credentials: credentials)\n        }\n        .eraseToAnyPublisher()\n        \n        return Update(state: state, fx: fx)\n    }\n}\n```\n\nStore will manage the lifecycle of any publishers returned by an Update; piping the actions they produce back into the store, producing new states, and cleaning them up when they complete.\n\n## Animations\n\nYou can also drive explicit animations as part of an Update.\n\nUse `Update.animation` to set an explicit [Animation](https://developer.apple.com/documentation/swiftui/animation) for this state update.\n\n```swift\nfunc update(\n    state: Model,\n    action: Action,\n    environment: Environment\n) -\u003e Update\u003cModel\u003e {\n    switch action {\n    // ...\n    case .authenticate(let credentials):\n        return Update(state: state).animation(.default)\n    }\n}\n```\n\nWhen you specify a transition or animation as part of an Update, Store will use that animation when setting the state for the update.\n\n## Getting and setting state in views\n\nThere are a few different ways to work with Store in views.\n\n`Store.state` lets you reference the current state directly within views. It’s read-only, so this is the approach to take if your view just needs to read, and doesn’t need to change state.\n\n```swift\nText(store.state.text)\n```\n\n`Store.send(_)` lets you send actions to the store to change state. You might call send within a button action or event callback, for example.\n\n```swift\nButton(\"Set color to red\") {\n    store.send(AppAction.setColor(.red))\n}\n```\n\n## Bindings\n\n`StoreProtocol.binding(get:tag:)` lets you create a [binding](https://developer.apple.com/documentation/swiftui/binding) that represents some part of a store state. The `get` closure reads the state into a value, and the `tag` closure wraps the value set on the binding in an action. The result is a binding that can be passed to any vanilla SwiftUI view, changing state only through deterministic updates.\n\n```swift\nTextField(\n    \"Username\"\n    text: store.binding(\n        get: { state in state.username },\n        tag: { username in .setUsername(username) }\n    )\n)\n```\n\nBottom line, because Store is just an ordinary [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) and can produce bindings, you can write views exactly the same way you write vanilla SwiftUI views. No special magic! Properties, [@Binding](https://developer.apple.com/documentation/swiftui/binding), [@ObservedObject](https://developer.apple.com/documentation/swiftui/observedobject), [@StateObject](https://developer.apple.com/documentation/swiftui/stateobject) and [@EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject) all work as you would expect.\n\n\n## Creating scoped child components\n\nWe can also create `ViewStore`s that represent just a scoped part of the root store. You can think of them as being like a binding, but they expose a `StoreProtocol` interface, instead of a binding interface. This allows you to create apps from free-standing components that all have their own local state, actions, and update functions, but share the same underlying root store.\n\nImagine we have a SWiftUI child view that looks something like this:\n\n```swift\nenum ChildAction {\n    case increment\n}\n\nstruct ChildModel: ModelProtocol {\n    var count: Int = 0\n\n    static func update(\n        state: ChildModel,\n        action: ChildAction,\n        environment: Void\n    ) -\u003e Update\u003cChildModel\u003e {\n        switch action {\n        case .increment:\n            var model = state\n            model.count = model.count + 1\n            return Update(state: model)\n        }\n    }\n}\n\nstruct ChildView: View {\n    var store: ViewStore\u003cChildModel\u003e\n\n    var body: some View {\n        VStack {\n            Text(\"Count \\(store.state.count)\")\n            Button(\n                \"Increment\",\n                action: {\n                    store.send(ChildAction.increment)\n                }\n            )\n        }\n    }\n}\n```\n\nTo integrate this child component with a parent component, we're going to need 3 functions:\n\n- A function to `get` a local state from the root state\n- A function to `set` a local state on a root state\n- A function to `tag` a local action so it becomes a root action\n\nTogether, these functions give us everything we need to map from child domain to a parent domain. Let's define them as static functions, so we have them all in one place.\n\n```swift\nstruct AppChildCursor {\n    /// Get child state from parent\n    static func get(_ state: ParentModel) -\u003e ChildModel {\n        state.child\n    }\n\n    /// Set child state on parent\n    static func set(_ state: ParentModel, _ child: ChildModel) -\u003e ParentModel {\n        var model = state\n        model.child = child\n        return model\n    }\n\n    /// Tag child action so it becomes a parent action\n    static func tag(_ action: ChildAction) -\u003e ParentAction {\n        switch action {\n        default:\n            return .child(action)\n        }\n    }\n}\n```\n\nOk, now that we have everything we need to map from the parent domain to the child domain, let's integrate the child view with the parent view.\n\nWe call the `store.viewStore(get:tag:)` method to create a scoped ViewStore from our store and pass it the appropriate cursor functions.\n\n```swift\nstruct ContentView: View {\n    @StateObject private var store: Store\u003cAppModel\u003e\n\n    var body: some View {\n        ChildView(\n            store: store.viewStore(\n                get: AppChildCursor.get,\n                tag: AppChildCursor.tag\n            )\n        )\n    }\n}\n```\n\nNote that `.viewStore(get:tag:)` is an extension of `StoreProtocol`, so you can call it on `Store` or `ViewStore` to create arbitrarily nested components!\n\nNext, we want to integrate the child's update function into the parent update function. Luckily, `ModelProtocol` synthesizes an `update(get:set:tag:state:action:environment)` function that automatically maps child state and actions to parent state and actions.\n\n```swift\nenum AppAction {\n    case child(ChildAction)\n}\n\nstruct AppModel: ModelProtocol {\n    var child = ChildModel()\n\n    static func update(\n        state: AppModel,\n        action: AppAction,\n        environment: AppEnvironment\n    ) -\u003e Update\u003cAppModel\u003e {\n        switch {\n        case .child(let action):\n            return update(\n                get: AppChildCursor.get,\n                set: AppChildCursor.set,\n                tag: AppChildCursor.tag,\n                state: state,\n                action: action,\n                environment: ()\n            )\n        }\n    }\n}\n```\n\nAnd that's it! We have successfully created an isolated child component and integrated it into a parent component. This tagging/update pattern also gives parent components an opportunity to intercept and handle child actions in special ways.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubconsciousnetwork%2Fobservablestore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsubconsciousnetwork%2Fobservablestore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubconsciousnetwork%2Fobservablestore/lists"}