Onward is a composable, action-based state management library for Swift, inspired by Redux and built around Swift's type system, result builders, and macros. It structures state changes as actions and reducers, separating business logic from state through an explicit Interactor layer.
@Storemacro: Declares a state container and auto-generates aProxysnapshot type and mutator actions for every stored property.@Interactormacro: Owns all business logic (actions, reducers, middleware) for a store, keeping it independently testable.- Composable Actions: Inline declarative style or macro-based (
@Action,@Reducer,@Middleware) — both first-class. - Middleware: Intercept actions for side effects; read state via an immutable
Proxysnapshot. - Async support:
AsyncActionandAsyncMiddlewarewithawait-based dispatch. - Dependency injection:
@Inwarddeclares dependencies onOnwardContainer;@Outwardresolves them — no singletons, fully testable. - Key-path dispatch: Dispatch actions via
store.dispatch(\.actionName)for clean, refactor-safe call sites.
Add Onward to your Package.swift:
.package(url: "https://github.com/pedro0x53/onward.git", from: "0.4.0")Then add Onward as a dependency for your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "Onward", package: "onward")
]
)Use @Store(Interactor.self) paired with @Observable. The macro generates a Proxy snapshot type and a mutator action for each stored var.
import Observation
import Onward
@Observable
@Store(ToDoInteractor.self)
final class ToDoStore {
var todos: [ToDo] = []
var isAlertPresented: Bool = false
}The @Interactor macro generates the required build() factory and init(). Declare your actions as computed properties — either inline (declarative) or with the @Action, @Reducer, and @Middleware macros.
import Onward
@Interactor
final class ToDoInteractor {
// Inline declarative style
var addItem: Action<ToDoStore, String, String> {
Action { title, description in
Middleware { proxy in
let newToDo = ToDo(title: title, description: description)
var todos = proxy.todos
todos.append(newToDo)
proxy.dispatch(\.todosMutator, todos)
}
}
}
var loadRemote: AsyncAction<ToDoStore> {
AsyncAction {
AsyncMiddleware { proxy in
let items = await self.apiClient.fetchItems()
proxy.dispatch(\.todosMutator, items)
}
AsyncReducer(setter: \.isAlertPresented) { true }
}
}
// Macro style — composes named reducers and middleware by key path
@Action(middlewares: \Self.fetchMiddleware, lateReducers: \Self.showAlertReducer)
var loadRemoteAction: AsyncAction<ToDoStore>
@Middleware(ToDoStore.self)
private func fetch(_ proxy: ToDoStore.Proxy) async {
let items = await apiClient.fetchItems()
proxy.dispatch(\.todosMutator, items)
}
@Reducer(ToDoStore.self, set: \.isAlertPresented)
func showAlert() async -> Bool { true }
}Declare dependencies with @Inward on an OnwardContainer extension. Resolve them anywhere with @Outward.
// Declaration (e.g. in Workers/APIClient.swift)
extension OnwardContainer {
@Inward var apiClient: APIClient = DefaultAPIClient()
}
// Resolution inside an Interactor
@Interactor
final class ToDoInteractor {
@Outward(\.apiClient) var apiClient: APIClient
// ...
}Override for tests using the charge static method:
OnwardContainer.charge(\.apiClient, MockAPIClient())Use key-path dispatch — no need to hold a direct reference to the action value.
struct ContentView: View {
@State private var store: ToDoStore = .init()
var body: some View {
List(store.todos) { todo in
Text(todo.title)
.onTapGesture {
// Dispatch on a nested store
todo.dispatch(\.toggleCompleted)
}
}
.toolbar {
Button("Add") {
store.dispatch(\.addItem, "New Task", "Description")
}
}
.task {
await store.dispatch(\.loadRemote)
}
}
}View ──dispatch──▶ Store ──interactor──▶ Interactor
│ │
(state) Actions / Reducers / Middleware
│ │
Proxy ◀────────────────── (read-only snapshot)
- Store holds observable state. The
@Storemacro wires it to its interactor and generates mutator actions. - Interactor owns all business logic. It never holds state; it only reads via
Proxyand writes via dispatch. - Middleware runs side effects before reducers apply. It receives an immutable
Proxysnapshot. - Reducer is a pure function that derives a new value from the current one.
- OnwardContainer is the dependency graph — declare entries with
@Inwardin a container extension, resolve them with@Outward, and override for tests withOnwardContainer.charge(\.key, mock).
See the full working SwiftUI app in Examples/Todos.
MIT