Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.prism.byescaleira.com/llms.txt

Use this file to discover all available pages before exploring further.

Prism’s architecture module implements a strict unidirectional data flow: state lives in a PrismStore, actions flow into a PrismReducer, and asynchronous side effects are described by PrismEffect. This pattern makes state changes predictable and easy to test because the same action always produces the same state transition.

Defining state and action types

Start by declaring the state and action types for your feature. Both must conform to Sendable so they can safely cross Swift concurrency boundaries. State should also conform to Equatable to enable efficient diffing.
struct CounterState: Sendable, Equatable {
    var count = 0
    var isLoading = false
}

enum CounterAction: Sendable {
    case increment
    case decrement
    case reset
    case fetchRemoteCount
    case remoteCountLoaded(Int)
}
Keep action names verb-based and past-tense for responses (remoteCountLoaded) versus present-tense for commands (fetchRemoteCount). This makes the intent of each action immediately clear.

Writing a reducer

A reducer processes one action at a time and mutates state in place. Prism supports two styles: a protocol conformance for structured types, and a closure for lightweight use.
struct CounterReducer: PrismReducer {
    func reduce(
        into state: inout CounterState,
        action: CounterAction
    ) -> PrismEffect<CounterAction> {
        switch action {
        case .increment:
            state.count += 1
            return .none
        case .decrement:
            state.count -= 1
            return .none
        case .reset:
            state.count = 0
            return .none
        case .fetchRemoteCount:
            state.isLoading = true
            return .none
        case .remoteCountLoaded(let value):
            state.count = value
            state.isLoading = false
            return .none
        }
    }
}

Creating a store

Pass your initial state and reducer to PrismStore. The store is annotated @MainActor and @Observable, so SwiftUI views observe it automatically.
let store = PrismStore(
    initialState: CounterState(),
    reducer: CounterReducer()
)

Sending actions

Call store.send(_:) to dispatch an action synchronously. Use store.dispatch(action:) in async contexts — it wraps send for convenience.
// Synchronous dispatch
store.send(.increment)
store.send(.decrement)

// Async context
Task {
    await store.dispatch(action: .reset)
}
send is @MainActor, so call it from the main actor — SwiftUI button actions and onAppear closures already satisfy this.

Async side effects with PrismEffect

When an action needs to perform async work — a network call, a timer, or any other side effect — return a PrismEffect from your reducer instead of PrismEffect.none. The store runs the effect and feeds any emitted actions back through the reducer.
case .fetchRemoteCount:
    state.isLoading = true
    return .run { send in
        let count = try await CounterAPI.fetchCount()
        send(.remoteCountLoaded(count))
    }
PrismEffect provides several factory methods:
MethodPurpose
.noneProduces no actions. Use this as the default return.
.send(_ action:)Emits a single action immediately.
.sequence(_ actions:)Emits a sequence of actions in order.
.run(priority:_:)Runs an async closure that calls send to emit actions.
.merge(_ effects:)Runs multiple effects concurrently.
// Emit multiple actions in sequence
return .sequence([.reset, .fetchRemoteCount])

// Run concurrent effects
return .merge(
    .run { send in await logEvent("started") },
    .run { send in
        let count = try await CounterAPI.fetchCount()
        send(.remoteCountLoaded(count))
    }
)

Adding middleware

Middleware runs after the reducer and is ideal for cross-cutting concerns like logging, analytics, or crash reporting. It receives the post-reduction state and the action, then returns any additional effects.
1

Conform to PrismMiddleware

struct LoggingMiddleware: PrismMiddleware {
    func run(
        state: CounterState,
        action: CounterAction
    ) -> PrismEffect<CounterAction> {
        print("[Log] action=\(action) count=\(state.count)")
        return .none
    }
}
2

Attach middleware to the store

Pass the middleware as a third argument to the store initializer. The reducer and middleware effects are merged automatically.
let store = PrismStore(
    initialState: CounterState(),
    reducer: CounterReducer(),
    middleware: LoggingMiddleware()
)
3

Compose multiple middlewares

Use PrismSideEffect.combine to group several middleware values into one.
let combined = PrismSideEffect<CounterState, CounterAction>.combine(
    PrismSideEffect { state, action in
        print("action: \(action)")
        return .none
    },
    PrismSideEffect { state, action in
        Analytics.track(action)
        return .none
    }
)

let store = PrismStore(
    initialState: CounterState(),
    reducer: CounterReducer(),
    middleware: combined
)

Using the built-in middleware chain

For middleware that needs to intercept and forward actions sequentially, use PrismMiddlewareChain with PrismChainableMiddleware conformances.
let chain = PrismMiddlewareChain()
    .add(PrismLoggingMiddleware())
    .add(PrismThrottleMiddleware(interval: .milliseconds(500)))

let middlewares = chain.build()
PrismLoggingMiddleware prints [PrismMiddleware] Action: … | State: … for every dispatched action. PrismThrottleMiddleware silently drops duplicate actions that arrive within the configured interval.

Scoping stores

Use store.scope(state:action:) to derive a child store from a parent. The child stays synchronized with the parent — actions sent to the child are lifted into parent actions, and parent state changes propagate back automatically.
// Parent feature
struct AppState: Sendable, Equatable {
    var counter: CounterState = .init()
}

enum AppAction: Sendable {
    case counter(CounterAction)
}

// Derive a CounterState / CounterAction child store
let counterStore: PrismStore<CounterState, CounterAction> = appStore.scope(
    state: \.counter,
    action: AppAction.counter
)
Three scope overloads are available:
OverloadWhen to use
scope(state:action:) — closureFull control over state projection and action lifting.
scope(state:action:) — key pathSelects state with a key path; lifts actions with a closure.
scope(state:) — key path onlySelects state with a key path; actions pass through unchanged.
// Key path + closure
let child = appStore.scope(
    state: \.counter,
    action: AppAction.counter
)

// Key path only (same action type)
let subStore = appStore.scope(state: \.counter)
Child stores hold a weak reference to their parent. If the parent store is deallocated, actions sent to the child are silently dropped.

Cancelling effects

Call store.cancelEffects() to immediately cancel all in-flight effect tasks managed by the store. This is useful when a view disappears and you want to stop any pending network requests.
.onDisappear {
    store.cancelEffects()
}