{"id":20977220,"url":"https://github.com/pfriedrix/flowkit","last_synced_at":"2026-04-25T01:03:18.321Z","repository":{"id":255373946,"uuid":"849407033","full_name":"pfriedrix/FlowKit","owner":"pfriedrix","description":"A lightweight Swift framework implementing the Redux pattern for iOS, providing centralized state management, reducers, effects, and seamless SwiftUI integration.","archived":false,"fork":false,"pushed_at":"2025-03-08T19:40:22.000Z","size":69,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-08T20:26:52.352Z","etag":null,"topics":["architecture","lightweight","reducer","redux-persist","swift","swiftui","tca"],"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/pfriedrix.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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-29T14:39:10.000Z","updated_at":"2025-03-08T19:38:46.000Z","dependencies_parsed_at":"2024-08-29T16:24:15.827Z","dependency_job_id":"591f1ee8-7cb3-456f-ac35-02976736ae77","html_url":"https://github.com/pfriedrix/FlowKit","commit_stats":null,"previous_names":["pfriedrix/redux","pfriedrix/flowkit"],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pfriedrix%2FFlowKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pfriedrix%2FFlowKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pfriedrix%2FFlowKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pfriedrix%2FFlowKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pfriedrix","download_url":"https://codeload.github.com/pfriedrix/FlowKit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243382116,"owners_count":20281998,"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":["architecture","lightweight","reducer","redux-persist","swift","swiftui","tca"],"created_at":"2024-11-19T04:57:40.821Z","updated_at":"2026-04-25T01:03:18.293Z","avatar_url":"https://github.com/pfriedrix.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FlowKit\n\nFlowKit is a Swift state-management library that implements Unidirectional Data Flow (UDF) on top of `@Observable` and Swift Concurrency. State, reducers, and effect dispatch are MainActor-isolated; async work runs off-actor and hops back via `Send`.\n\n```\nAction → Reducer → State → Effect → (async work) → Action\n```\n\n## Requirements\n\n- Swift 6.0 (Xcode 16+), Swift language mode `.v6`\n- iOS 17, macOS 14, watchOS 10, tvOS 17\n- Strict-concurrency clean: every `State` and `Action` must be `Sendable`\n\n## Installation\n\nAdd FlowKit to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/pfriedrix/FlowKit\", from: \"0.3.2\")\n],\ntargets: [\n    .target(name: \"App\", dependencies: [\"FlowKit\"])\n]\n```\n\nOr in Xcode: **File → Add Packages…**, enter `https://github.com/pfriedrix/FlowKit`, pick a version, add the **FlowKit** product to your target.\n\n## Quick start\n\n```swift\nimport FlowKit\nimport SwiftUI\n\nstruct CounterReducer: Reducer {\n    struct State: Equatable, Sendable { var count = 0 }\n    enum Action: Sendable { case increment, decrement }\n\n    func reduce(into state: inout State, action: Action) -\u003e Effect\u003cAction\u003e {\n        switch action {\n        case .increment: state.count += 1; return .none\n        case .decrement: state.count -= 1; return .none\n        }\n    }\n}\n\nstruct CounterView: View {\n    @State private var store = Store(initial: .init(), reducer: CounterReducer())\n\n    var body: some View {\n        VStack {\n            Text(\"\\(store.state.count)\")\n            Button(\"+\") { store.send(.increment) }\n            Button(\"-\") { store.send(.decrement) }\n        }\n    }\n}\n```\n\n`Store` is `@Observable`, so SwiftUI tracks `state` automatically.\n\n## Store\n\n```swift\n@MainActor\n@Observable\npublic final class Store\u003cR: Reducer\u003e {\n    public var state: State\n    public required init(initial: State, reducer: R)\n    public func send(_ action: Action)\n}\n```\n\n`send(_:)` runs the reducer synchronously on MainActor, commits the new state, then schedules any returned effect. In-flight `.run` effects are tracked in a per-store registry keyed by UUID and cancelled on `deinit`.\n\nA second initializer is available when `State: Storable` — see [Persistence](#persistence).\n\n## Reducer\n\n```swift\npublic protocol Reducer\u003cState, Action\u003e: Sendable {\n    associatedtype State: Sendable\n    associatedtype Action: Sendable\n\n    @MainActor\n    func reduce(into state: inout State, action: Action) -\u003e Effect\u003cAction\u003e\n}\n```\n\n## Effect\n\n`Effect\u003cAction\u003e` is the only return type from a reducer. It carries one of:\n\n- `.none` — no side effect\n- `.send(Action)` — dispatch a follow-up action\n- `.merge(Action...)` — dispatch several actions in order\n- `.run { send in … }` — run an async closure, optionally `throws`, with an optional `catch` handler\n- `.cancel(id:)` — cancel an in-flight cancellable run\n\n```swift\ncase .fetchUser(let id):\n    return .run { send in\n        let user = try await api.user(id: id)\n        await send(.userLoaded(user))\n    } catch: { error, send in\n        await send(.userFailed(error))\n    }\n```\n\nInside a `.run`, `send` is a `Send\u003cAction\u003e` you call as a function. Each call hops to MainActor and dispatches into this store. To dispatch into a *different* shared store, pass a key path:\n\n```swift\nreturn .run { send in\n    let value = await fetchValue()\n    await send(\\.analyticsStore, action: .track(value))\n}\n```\n\nIf you only need fire-and-forget dispatch into another store from a reducer, use `Effect.send(_:action:)`:\n\n```swift\nreturn .send(\\.analyticsStore, action: .screenViewed)\n```\n\n### Effect modifiers\n\n```swift\n.cancellable(id: SearchID(), cancelInFlight: true)\n.animation(.spring())\n```\n\n- `.cancellable(id:cancelInFlight:)` — registers the `.run` task in the store's MainActor task registry under `id`. Pair with `Effect.cancel(id:)` to cancel it. Set `cancelInFlight: true` to cancel any prior task with the same id before this one starts.\n- `Effect.cancel(id:)` — pure-data effect; cancellation happens synchronously on MainActor when the store handles it, so it's safe to dispatch in the same `send` chain that registered the task.\n- `.animation(_:)` — wraps the action dispatches this effect performs in `withAnimation(_:)`. Pass `nil` to clear an inherited animation.\n\n\u003e Cancellation is cooperative. The async body must observe `Task.isCancelled` (e.g. via `try await Task.sleep`, `try Task.checkCancellation()`) for cancellation to actually stop work.\n\n## Persistence\n\n### Storable\n\n```swift\npublic protocol Storable {\n    func save()\n    static func load() -\u003e Self?\n}\n```\n\nWhen `State: Storable`, use the convenience initializer to wire automatic save-on-action:\n\n```swift\nlet store = Store(reducer: AppReducer(), default: AppState())\n```\n\nThis restores from `State.load()` if available, otherwise seeds with `default` and saves it. Subsequent `send(_:)` calls fire `state.save()` on a per-store serial background queue, so encoding stays off MainActor.\n\n### Persistable\n\n`Persistable: Storable, Codable` ships default JSON + `UserDefaults` implementations keyed by `String(reflecting: Self.self)`. Conformance is one line:\n\n```swift\nstruct AppState: Persistable, Equatable {\n    var count = 0\n    var isLoggedIn = false\n}\n```\n\nFor custom backing stores (Keychain, files, SwiftData…) implement `Storable` directly.\n\n## Dependency injection\n\nFlowKit ships a SwiftUI-style task-local registry: `StoreValues` is to stores what `EnvironmentValues` is to environment values.\n\n### @Inject\n\n`@Inject` is the recommended way to register a shared store. Apply it to a typed, initialized property inside `extension StoreValues`:\n\n```swift\nextension StoreValues {\n    @Inject var counterStore: Store\u003cCounterReducer\u003e = .init(\n        initial: .init(),\n        reducer: .init()\n    )\n}\n```\n\nThe macro expands to:\n\n```swift\nextension StoreValues {\n    fileprivate struct __Store_counterStore: StoreKey {\n        @MainActor static let defaultValue: Store\u003cCounterReducer\u003e =\n            .init(initial: .init(), reducer: .init())\n    }\n    var counterStore: Store\u003cCounterReducer\u003e {\n        get { self[__Store_counterStore.self] }\n        set { self[__Store_counterStore.self] = newValue }\n    }\n}\n```\n\n### @Shared\n\nPull a registered store into a SwiftUI view:\n\n```swift\nstruct CounterView: View {\n    @Shared(\\.counterStore) var store\n\n    var body: some View {\n        Text(\"\\(store.state.count)\")\n    }\n}\n```\n\n`@Shared` resolves the store from `StoreValues` once, at init time. The wrapped value is read-only — for test-time overrides use `StoreValues.withValues { ... }`:\n\n```swift\nStoreValues.withValues { values in\n    values.counterStore = Store(initial: .init(count: 42), reducer: CounterReducer())\n} operation: {\n    // store reads inside this closure see the override\n}\n```\n\nA `@Sendable` async overload exists for use inside `Task { … }`.\n\n## SwiftUI bindings\n\n`Store` exposes four `binding(...)` overloads for driving SwiftUI controls:\n\n```swift\n// 1. Custom getter and action-returning setter\nstore.binding(get: { someValue }, set: { .didChange($0) })\n\n// 2. Getter receives the state\nstore.binding(get: \\.someValue, set: { .didChange($0) })\n\n// 3. KeyPath + action factory\nstore.binding(for: \\.username, set: { .usernameChanged($0) })\n\n// 4. KeyPath + a single action dispatched on every change\nstore.binding(for: \\.isPresented, set: .didDismiss)\n```\n\nGetters capture the store strongly so SwiftUI never reads through a dangling reference; setters capture weakly so a discarded `Binding` does not extend the store's lifetime.\n\n## Logging\n\nFlowKit logs every action and resolved state through `os.Logger` (subsystem `flow-kit`, category `store-events`). Configure verbosity and action formatting at process start:\n\n```swift\nimport os\nimport FlowKit\n\nLogger.logLevel = .info          // .debug | .info | .error | .fault\nLogger.formatStyle = .short      // .full | .short | .abbreviated\n```\n\nBoth properties are thread-safe.\n\n## Testing\n\nTests run on `@MainActor` and create stores directly. The test target ships a `waitForStateChange` helper for asserting state after async effects:\n\n```swift\nlet store = Store(initial: .init(), reducer: CounterReducer())\nstore.send(.fetchData)\n\ntry await waitForStateChange(timeout: 1) {\n    store.state.data != nil\n}\n```\n\nIt observes `@Observable` notifications and falls back to short polling, so it returns as soon as the predicate flips.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpfriedrix%2Fflowkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpfriedrix%2Fflowkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpfriedrix%2Fflowkit/lists"}