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.

PrismArchitecture implements the unidirectional data flow (UDF) pattern for Apple-platform apps. State lives in a single observable store. Actions describe intent. A reducer applies each action to the state and returns an effect for any asynchronous work. Middleware intercepts actions after reduction to handle cross-cutting concerns like logging or analytics. A type-safe router manages navigation across push, sheet, and full-screen presentations—all without NavigationPath stringly-typed values.

Installation

import PrismArchitecture

Core concepts

Data flows in one direction:
Action → PrismStore.send() → PrismReducer → new State + PrismEffect

                                         PrismEffect emits more Actions
Your views read store.state and call store.send(action). Nothing else modifies state.

Counter example

The following walkthrough shows the complete setup for a counter feature.
1

Define state and actions

State is a plain Sendable struct. Actions are a Sendable enum.
struct CounterState: Sendable, Equatable {
    var count = 0
    var isLoading = false
}

enum CounterAction: Sendable {
    case increment
    case decrement
    case reset
    case fetchRemoteCount
    case remoteCountLoaded(Int)
}
2

Implement a reducer

The reducer mutates state in place and returns a PrismEffect for any async work.
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 .run { send in
                let count = try await CounterAPI.fetchCount()
                send(.remoteCountLoaded(count))
            }

        case .remoteCountLoaded(let count):
            state.isLoading = false
            state.count = count
            return .none
        }
    }
}
3

Create the store

@State var store = PrismStore(
    initialState: CounterState(),
    reducer: CounterReducer()
)
4

Connect to a SwiftUI view

The store is @Observable, so SwiftUI automatically re-renders when state changes.
struct CounterView: View {
    var store: PrismStore<CounterState, CounterAction>

    var body: some View {
        VStack {
            Text("\(store.state.count)")
                .font(.largeTitle)

            HStack {
                Button("-") { store.send(.decrement) }
                Button("+") { store.send(.increment) }
                Button("Reset") { store.send(.reset) }
            }

            Button("Fetch") {
                store.send(.fetchRemoteCount)
            }
            .disabled(store.state.isLoading)
        }
    }
}

PrismEffect

PrismEffect describes side effects that produce zero or more actions asynchronously. The store executes effects and feeds any emitted actions back through the reducer.
// No side effects
return .none

// Emit a single action immediately
return .send(.remoteCountLoaded(42))

// Run an async operation
return .run { send in
    let count = try await CounterAPI.fetchCount()
    send(.remoteCountLoaded(count))
}

// Merge multiple effects (runs concurrently)
return .merge(
    .run { send in await logEvent(action) },
    .run { send in
        let count = try await CounterAPI.fetchCount()
        send(.remoteCountLoaded(count))
    }
)

Scoped stores

Use scope(state:action:) to derive a child store that presents a sub-feature’s state and action types. The child stays in sync with the parent—actions flow up through fromLocalAction, and state changes project back down.
// Parent store owns AppState / AppAction
let profileStore: PrismStore<ProfileState, ProfileAction> = store.scope(
    state: \.profile,
    action: AppAction.profile
)

// Pass only what the child view needs
ProfileView(store: profileStore)

Middleware

PrismMiddleware runs after each reduction and is ideal for cross-cutting concerns. It receives the post-reduction state and the action, and returns a PrismEffect for any additional work.
struct AnalyticsMiddleware: PrismMiddleware {
    func run(
        state: CounterState,
        action: CounterAction
    ) -> PrismEffect<CounterAction> {
        switch action {
        case .increment, .decrement:
            return .run { _ in
                Analytics.track("counter_changed", value: state.count)
            }
        default:
            return .none
        }
    }
}

// Attach middleware when creating the store
let store = PrismStore(
    initialState: CounterState(),
    reducer: CounterReducer(),
    middleware: AnalyticsMiddleware()
)
You can also use the closure-based PrismSideEffect for inline middleware without a dedicated type:
let logging = PrismSideEffect<CounterState, CounterAction> { state, action in
    return .run { _ in print("Action: \(action), Count: \(state.count)") }
}

Type-safe navigation

PrismRouter manages a push stack, a sheet, and a full-screen cover under a single observable object. Your routes are any type that conforms to PrismRoutable (Hashable + Identifiable + Sendable).
enum AppRoute: PrismRoutable {
    case home
    case detail(id: String)
    case settings
}

let router = PrismRouter<AppRoute>()

// Push onto the navigation stack
router.push(.detail(id: "42"))

// Present as a sheet
router.present(.settings)

// Present full-screen
router.fullScreen(.onboarding)

// Dismiss the topmost layer
router.dismiss()

// Return to root
router.root()
router.topRoute always returns the currently visible route, checking the stack first, then the full-screen cover, then the sheet.

State persistence

PrismPersistMiddleware automatically saves state after every action using a PrismPersistenceStrategy. Three strategies ship out of the box:
StrategyBacking storeBest for
PrismDiskPersistenceDocuments directory (JSON file)Large, non-sensitive state
PrismUserDefaultsPersistenceUserDefaultsSmall, lightweight preferences
PrismKeychainPersistenceSystem KeychainSensitive tokens and credentials
// Persist to disk automatically on every action
let store = PrismStore(
    initialState: AppState(),
    reducer: AppReducer(),
    middleware: PrismPersistMiddleware(
        strategy: PrismDiskPersistence(),
        key: "app_state"
    )
)

// Restore on next launch
let strategy = PrismDiskPersistence()
let saved: AppState? = try? strategy.load(key: "app_state")
let initialState = saved ?? AppState()
PrismKeychainPersistence stores data under the PrismStatePersistence service name. Changing the key or service name between app versions will cause previously saved data to become inaccessible.