{"id":13536840,"url":"https://github.com/StevenLambion/SwiftDux","last_synced_at":"2025-04-02T03:31:11.852Z","repository":{"id":41110560,"uuid":"191248620","full_name":"StevenLambion/SwiftDux","owner":"StevenLambion","description":"Predictable state management for SwiftUI applications.","archived":false,"fork":false,"pushed_at":"2021-07-10T16:36:59.000Z","size":6108,"stargazers_count":153,"open_issues_count":1,"forks_count":9,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-11-03T01:33:24.025Z","etag":null,"topics":["architecture","combine","ios","macos","reactive","redux","state","state-management","swift","swiftui","xcode"],"latest_commit_sha":null,"homepage":"http://stevenlambion.github.io/SwiftDux","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/StevenLambion.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}},"created_at":"2019-06-10T21:32:17.000Z","updated_at":"2024-04-11T11:14:16.000Z","dependencies_parsed_at":"2022-08-28T23:31:47.726Z","dependency_job_id":null,"html_url":"https://github.com/StevenLambion/SwiftDux","commit_stats":null,"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StevenLambion%2FSwiftDux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StevenLambion%2FSwiftDux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StevenLambion%2FSwiftDux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StevenLambion%2FSwiftDux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/StevenLambion","download_url":"https://codeload.github.com/StevenLambion/SwiftDux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246751079,"owners_count":20827831,"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","combine","ios","macos","reactive","redux","state","state-management","swift","swiftui","xcode"],"created_at":"2024-08-01T09:00:50.381Z","updated_at":"2025-04-02T03:31:11.144Z","avatar_url":"https://github.com/StevenLambion.png","language":"Swift","funding_links":[],"categories":["Samples","State"],"sub_categories":["Content"],"readme":"# SwiftDux\n\n\u003e Predictable state management for SwiftUI applications.\n\n[![Swift Version][swift-image]][swift-url]\n![Platform Versions][ios-image]\n[![Github workflow][github-workflow-image]](https://github.com/StevenLambion/SwiftDux/actions)\n[![codecov][codecov-image]](https://codecov.io/gh/StevenLambion/SwiftDux)\n\nSwiftDux is a state container inspired by Redux and built on top of Combine and SwiftUI. It helps you write applications with predictable, consistent, and highly testable logic using a single source of truth.\n\n# Installation\n\n## Prerequisites\n- Xcode 12+\n- Swift 5.3+\n- iOS 14+, macOS 11.0+, tvOS 14+, or watchOS 7+\n\n## Install via Xcode:\n\nSearch for SwiftDux in Xcode's Swift Package Manager integration.\n\n## Install via the Swift Package Manager:\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n  dependencies: [\n    .Package(url: \"https://github.com/StevenLambion/SwiftDux.git\", from: \"2.0.0\")\n  ]\n)\n```\n\n# Demo Application\n\nTake a look at the [Todo Example App](https://github.com/StevenLambion/SwiftUI-Todo-Example) to see how SwiftDux works.\n\n# Getting Started\n\nSwiftDux helps build SwiftUI-based applications around an [elm-like architecture](https://guide.elm-lang.org/architecture/) using a single, centralized state container. It has 4 basic constructs:\n\n- **State** - An immutable, single source of truth within the application.\n- **Action** - Describes a single change of the state.\n- **Reducer** - Returns a new state by consuming the previous one with an action.\n- **View** - The visual representation of the current state.\n\n\u003cdiv style=\"text-align:center\"\u003e\n  \u003cimg src=\"Guides/Images/architecture.jpg\" width=\"400\"/\u003e\n\u003c/div\u003e\n\n## State\n\nThe state is an immutable structure acting as the single source of truth within the application.\n\nBelow is an example of a todo app's state. It has a root `AppState` as well as an ordered list of `TodoItem` objects.\n\n```swift\nimport SwiftDux\n\ntypealias StateType = Equatable \u0026 Codable\n\nstruct AppState: StateType {\n  todos: OrderedState\u003cTodoItem\u003e\n}\n\nstruct TodoItem: StateType, Identifiable {\n  var id: String,\n  var text: String\n}\n```\n\n## Actions\n\nAn action is a dispatched event to mutate the application's state. Swift's enum type is ideal for actions, but structs and classes could be used as well.\n\n```swift\nimport SwiftDux\n\nenum TodoAction: Action {\n  case addTodo(text: String)\n  case removeTodos(at: IndexSet)\n  case moveTodos(from: IndexSet, to: Int)\n}\n```\n\n## Reducers\n\nA reducer consumes an action to produce a new state.\n\n```swift\nfinal class TodosReducer: Reducer {\n\n  func reduce(state: AppState, action: TodoAction) -\u003e AppState {\n    var state = state\n    switch action {\n    case .addTodo(let text):\n      let id = UUID().uuidString\n      state.todos.append(TodoItemState(id: id, text: text))\n    case .removeTodos(let indexSet):\n      state.todos.remove(at: indexSet)\n    case .moveTodos(let indexSet, let index):\n      state.todos.move(from: indexSet, to: index)\n    }\n    return state\n  }\n}\n```\n\n## Store\n\nThe store manages the state and notifies the views of any updates.\n\n```swift\nimport SwiftDux\n\nlet store = Store(\n  state: AppState(todos: OrderedState()),\n  reducer: AppReducer()\n)\n\nwindow.rootViewController = UIHostingController(\n  rootView: RootView().provideStore(store)\n)\n```\n\n## Middleware\nSwiftDux supports middleware to extend functionality. The SwiftDuxExtras module provides two built-in middleware to get started:\n\n- `PersistStateMiddleware` persists and restores the application state between sessions.\n- `PrintActionMiddleware` prints out each dispatched action for debugging purposes.\n\n```swift\nimport SwiftDux\n\nlet store = Store(\n  state: AppState(todos: OrderedState()),\n  reducer: AppReducer(),\n  middleware: PrintActionMiddleware())\n)\n\nwindow.rootViewController = UIHostingController(\n  rootView: RootView().provideStore(store)\n)\n```\n\n## Composing Reducers, Middleware, and Actions\nYou may compose a set of reducers, actions, or middleware into an ordered chain using the '+' operator.\n\n```swift\n// Break up an application into smaller modules by composing reducers.\nlet rootReducer = AppReducer() + NavigationReducer()\n\n// Add multiple middleware together.\nlet middleware = \n  PrintActionMiddleware() +\n  PersistStateMiddleware(JSONStatePersistor()\n\nlet store = Store(\n  state: AppState(todos: OrderedState()),\n  reducer: reducer,\n  middleware: middleware\n)\n```\n\n## ConnectableView\n\nThe `ConnectableView` protocol provides a slice of the application state to your views using the functions `map(state:)` or  `map(state:binder:)`. It automatically updates the view when the props value has changed.\n\n```swift\nstruct TodosView: ConnectableView {\n  struct Props: Equatable {\n    var todos: [TodoItem]\n  }\n\n  func map(state: AppState) -\u003e Props? {\n    Props(todos: state.todos)\n  }\n\n  func body(props: OrderedState\u003cTodo\u003e): some View {\n    List {\n      ForEach(todos) { todo in\n        TodoItemRow(item: todo)\n      }\n    }\n  }\n}\n```\n\n## ActionBinding\u003c_\u003e\n\nUse the `map(state:binder:)` method on the `ConnectableView` protocol to bind an action to the props object. It can also be used to bind an updatable state value with an action.\n\n```swift\nstruct TodosView: ConnectableView {\n  struct Props: Equatable {\n    var todos: [TodoItem]\n    @ActionBinding var newTodoText: String\n    @ActionBinding var addTodo: () -\u003e ()\n  }\n\n  func map(state: AppState, binder: ActionBinder) -\u003e OrderedState\u003cTodo\u003e? {\n    Props(\n      todos: state.todos,\n      newTodoText: binder.bind(state.newTodoText) { TodoAction.setNewTodoText($0) },\n      addTodo: binder.bind { TodoAction.addTodo() }\n    )\n  }\n\n  func body(props: OrderedState\u003cTodo\u003e): some View {\n    List {\n      TextField(\"New Todo\", text: props.$newTodoText, onCommit: props.addTodo) \n      ForEach(todos) { todo in\n        TodoItemRow(item: todo)\n      }\n    }\n  }\n}\n```\n\n## Action Plans\nAn `ActionPlan` is a special kind of action that can be used to group other actions together or perform any kind of async logic outside of a reducer. It's also useful for actions that may require information about the state before it can be dispatched.\n\n```swift\n/// Dispatch multiple actions after checking the current state of the application.\nlet plan = ActionPlan\u003cAppState\u003e { store in\n  guard store.state.someValue == nil else { return }\n  store.send(actionA)\n  store.send(actionB)\n  store.send(actionC)\n}\n\n/// Subscribe to services and return a publisher that sends actions to the store.\nlet plan = ActionPlan\u003cAppState\u003e { store in\n  userLocationService\n    .publisher\n    .map { LocationAction.updateUserLocation($0) }\n}\n```\n\n## Action Dispatching\nYou can access the `ActionDispatcher` of the store through the environment values. This allows you to dispatch actions from any view.\n\n```swift\nstruct MyView: View {\n  @Environment(\\.actionDispatcher) private var dispatch\n\n  var body: some View {\n    MyForm.onAppear { dispatch(FormAction.prepare) }\n  }\n}\n```\n\nIf it's an ActionPlan that's meant to be kept alive through a publisher, then you'll want to send it as a cancellable. The action below subscribes\nto the store, so it can keep a list of albums updated when the user applies different queries.\n\n```swift\nextension AlbumListAction {\n  var updateAlbumList: Action {\n    ActionPlan\u003cAppState\u003e { store in\n      store\n        .publish { $0.albumList.query }\n        .debounce(for: .seconds(1), scheduler: RunLoop.main)\n        .map { AlbumService.all(query: $0) }\n        .switchToLatest()\n        .catch { Just(AlbumListAction.setError($0) }\n        .map { AlbumListAction.setAlbums($0) }\n    }\n  }\n}\n\nstruct AlbumListContainer: ConnectableView {\n  @Environment(\\.actionDispatcher) private var dispatch\n  @State private var cancellable: Cancellable? = nil\n  \n  func map(state: AppState) -\u003e [Album]? {\n    state.albumList.albums\n  }\n\n  func body(props: [Album]) -\u003e some View {\n    AlbumsList(albums: props).onAppear { \n      cancellable = dispatch.sendAsCancellable(AlbumListAction.updateAlbumList)\n    }\n  }\n}\n```\n\nThe above can be further simplified by using the built-in `onAppear(dispatch:)` method instead. This method not only dispatches regular actions, but it automatically handles cancellable ones. By default, the action will cancel itself when the view is destroyed.\n\n```swift\nstruct AlbumListContainer: ConnectableView {\n  \n  func map(state: AppState) -\u003e [Album]? {\n    Props(state.albumList.albums)\n  }\n\n  func body(props: [Album]) -\u003e some View {\n    AlbumsList(albums: props).onAppear(dispatch: AlbumListAction.updateAlbumList)\n  }\n}\n```\n\n## Previewing Connected Views\nTo preview a connected view by itself use the `provideStore(_:)` method inside the preview.\n\n```swift\n#if DEBUG\npublic enum TodoRowContainer_Previews: PreviewProvider {\n  static var store: Store\u003cTodoList\u003e {\n    Store(\n      state: TodoList(\n        id: \"1\",\n        name: \"TodoList\",\n        todos: .init([\n          Todo(id: \"1\", text: \"Get milk\")\n        ])\n      ),\n      reducer: TodosReducer()\n    )\n  }\n  \n  public static var previews: some View {\n    TodoRowContainer(id: \"1\")\n      .provideStore(store)\n  }\n}\n#endif\n```\n\n[swift-image]: https://img.shields.io/badge/swift-5.3-orange.svg\n[ios-image]: https://img.shields.io/badge/platforms-iOS%2014%20%7C%20macOS%2011.0%20%7C%20tvOS%2014%20%7C%20watchOS%207-222.svg\n[swift-url]: https://swift.org/\n[license-image]: https://img.shields.io/badge/License-MIT-blue.svg\n[license-url]: LICENSE\n[codebeat-image]: https://codebeat.co/badges/c19b47ea-2f9d-45df-8458-b2d952fe9dad\n[codebeat-url]: https://codebeat.co/projects/github-com-vsouza-awesomeios-com\n[github-workflow-image]: https://github.com/StevenLambion/SwiftDux/workflows/build/badge.svg\n[codecov-image]: https://codecov.io/gh/StevenLambion/SwiftDux/branch/master/graph/badge.svg\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FStevenLambion%2FSwiftDux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FStevenLambion%2FSwiftDux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FStevenLambion%2FSwiftDux/lists"}