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
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.
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)
}
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
}
}
}
Create the store
@State var store = PrismStore(
initialState: CounterState(),
reducer: CounterReducer()
)
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:
| Strategy | Backing store | Best for |
|---|
PrismDiskPersistence | Documents directory (JSON file) | Large, non-sensitive state |
PrismUserDefaultsPersistence | UserDefaults | Small, lightweight preferences |
PrismKeychainPersistence | System Keychain | Sensitive 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.