SwiftDux
  • August 6, 2025

SwiftDux 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.

Installation


Prerequisites

  • Xcode 12+
  • Swift 5.3+
  • iOS 14+, macOS 11.0+, tvOS 14+, or watchOS 7+

Install via Xcode:

Search for SwiftDux in Xcode’s Swift Package Manager integration.

Install via the Swift Package Manager

import PackageDescription

let package = Package(
dependencies: [
.Package(url: “https://github.com/StevenLambion/SwiftDux.git”, from: “2.0.0”)
]
)

Demo Application


Take a look at the Todo Example App to see how SwiftDux works.

Getting Started


SwiftDux helps build SwiftUI-based applications around an elm-like architecture using a single, centralized state container. It has 4 basic constructs:

  • State – An immutable, single source of truth within the application.
  • Action – Describes a single change of the state.
  • Reducer – Returns a new state by consuming the previous one with an action.
  • View – The visual representation of the current state.

State


The state is an immutable structure acting as the single source of truth within the application.

Below is an example of a todo app’s state. It has a root AppState as well as an ordered list of TodoItem objects.

import SwiftDux

typealias StateType = Equatable & Codable

struct AppState: StateType {
todos: OrderedState<TodoItem>
}

struct TodoItem: StateType, Identifiable {
var id: String,
var text: String
}

Actions


An 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.

import SwiftDux

enum TodoAction: Action {
case addTodo(text: String)
case removeTodos(at: IndexSet)
case moveTodos(from: IndexSet, to: Int)
}

Reducers


A reducer consumes an action to produce a new state.

final class TodosReducer: Reducer {

func reduce(state: AppState, action: TodoAction) -> AppState {
var state = state
switch action {
case .addTodo(let text):
let id = UUID().uuidString
state.todos.append(TodoItemState(id: id, text: text))
case .removeTodos(let indexSet):
state.todos.remove(at: indexSet)
case .moveTodos(let indexSet, let index):
state.todos.move(from: indexSet, to: index)
}
return state
}
}

Store


The store manages the state and notifies the views of any updates.

import SwiftDux

let store = Store(
state: AppState(todos: OrderedState()),
reducer: AppReducer()
)

window.rootViewController = UIHostingController(
rootView: RootView().provideStore(store)
)

Middleware


SwiftDux supports middleware to extend functionality. The SwiftDuxExtras module provides two built-in middleware to get started:

  • PersistStateMiddleware persists and restores the application state between sessions.
  • PrintActionMiddleware prints out each dispatched action for debugging purposes.

import SwiftDux

let store = Store(
state: AppState(todos: OrderedState()),
reducer: AppReducer(),
middleware: PrintActionMiddleware())
)

window.rootViewController = UIHostingController(
rootView: RootView().provideStore(store)
)

Composing Reducers, Middleware, and Actions


You may compose a set of reducers, actions, or middleware into an ordered chain using the ‘+’ operator.

// Break up an application into smaller modules by composing reducers.
let rootReducer = AppReducer() + NavigationReducer()

// Add multiple middleware together.
let middleware =
PrintActionMiddleware() +
PersistStateMiddleware(JSONStatePersistor()

let store = Store(
state: AppState(todos: OrderedState()),
reducer: reducer,
middleware: middleware
)

ConnectableView


The 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.

struct TodosView: ConnectableView {
struct Props: Equatable {
var todos: [TodoItem]
}

func map(state: AppState) -> Props? {
Props(todos: state.todos)
}

func body(props: OrderedState<Todo>): some View {
List {
ForEach(todos) { todo in
TodoItemRow(item: todo)
}
}
}
}

ActionBinding<_>


Use 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.

struct TodosView: ConnectableView {
struct Props: Equatable {
var todos: [TodoItem]
@ActionBinding var newTodoText: String
@ActionBinding var addTodo: () -> ()
}

func map(state: AppState, binder: ActionBinder) -> OrderedState<Todo>? {
Props(
todos: state.todos,
newTodoText: binder.bind(state.newTodoText) { TodoAction.setNewTodoText($0) },
addTodo: binder.bind { TodoAction.addTodo() }
)
}

func body(props: OrderedState<Todo>): some View {
List {
TextField(“New Todo”, text: props.$newTodoText, onCommit: props.addTodo)
ForEach(todos) { todo in
TodoItemRow(item: todo)
}
}
}
}

Action Plans


An 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.

/// Dispatch multiple actions after checking the current state of the application.
let plan = ActionPlan<AppState> { store in
guard store.state.someValue == nil else { return }
store.send(actionA)
store.send(actionB)
store.send(actionC)
}

/// Subscribe to services and return a publisher that sends actions to the store.
let plan = ActionPlan<AppState> { store in
userLocationService
.publisher
.map { LocationAction.updateUserLocation($0) }
}

Action Dispatching


You can access the ActionDispatcher of the store through the environment values. This allows you to dispatch actions from any view.

struct MyView: View {
@Environment(\.actionDispatcher) private var dispatch

var body: some View {
MyForm.onAppear { dispatch(FormAction.prepare) }
}
}

If 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 to the store, so it can keep a list of albums updated when the user applies different queries.

extension AlbumListAction {
var updateAlbumList: Action {
ActionPlan<AppState> { store in
store
.publish { $0.albumList.query }
.debounce(for: .seconds(1), scheduler: RunLoop.main)
.map { AlbumService.all(query: $0) }
.switchToLatest()
.catch { Just(AlbumListAction.setError($0) }
.map { AlbumListAction.setAlbums($0) }
}
}
}

struct AlbumListContainer: ConnectableView {
@Environment(\.actionDispatcher) private var dispatch
@State private var cancellable: Cancellable? = nil

func map(state: AppState) -> [Album]? {
state.albumList.albums
}

func body(props: [Album]) -> some View {
AlbumsList(albums: props).onAppear {
cancellable = dispatch.sendAsCancellable(AlbumListAction.updateAlbumList)
}
}
}

The 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.

struct AlbumListContainer: ConnectableView {

func map(state: AppState) -> [Album]? {
Props(state.albumList.albums)
}

func body(props: [Album]) -> some View {
AlbumsList(albums: props).onAppear(dispatch: AlbumListAction.updateAlbumList)
}
}

Previewing Connected Views


To preview a connected view by itself use the provideStore(_:) method inside the preview.

#if DEBUG
public enum TodoRowContainer_Previews: PreviewProvider {
static var store: Store<TodoList> {
Store(
state: TodoList(
id: “1”,
name: “TodoList”,
todos: .init([
Todo(id: “1”, text: “Get milk”)
])
),
reducer: TodosReducer()
)
}

public static var previews: some View {
TodoRowContainer(id: “1”)
.provideStore(store)
}
}
#endif

GitHub


View Github

YOU MIGHT ALSO LIKE...
SwiftUI Shapes

Collection of custom shapes Regular Polygons  

ScrollViewIfNeeded

A SwiftUI ScrollView that only scrolls if the content doesn't fit in the View Installation Requirements iOS 13+ Swift Package ...

SwiftUITrackableScrollView

Swift Package Manager The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated ...

FancyScrollView

FancyScrollView  

Verge.swift

  Using StoreReader or @Reading in SwiftUI In SwiftUI, there are two ways to observe a Store: using the StoreReader view ...