Skip to main content

Advanced Features

PrismArchitecture ships with several advanced tools for debugging, persistence, and testing.

State Persistence

Persist state across app launches using PrismPersistenceStrategy. Three built-in strategies are provided:

Disk Persistence

Saves state as JSON files to the Documents directory:
Disk Persistence
let persistence = PrismDiskPersistence()

// Save
try persistence.save(appState, key: "app_state")

// Load
let loaded: AppState? = try persistence.load(key: "app_state")

// Clear
try persistence.clear(key: "app_state")
You can specify a custom directory:
let persistence = PrismDiskPersistence(
    directory: FileManager.default.temporaryDirectory
)

UserDefaults Persistence

Stores state in UserDefaults for lightweight data:
UserDefaults Persistence
let persistence = PrismUserDefaultsPersistence()

try persistence.save(settings, key: "user_settings")
let settings: Settings? = try persistence.load(key: "user_settings")

Keychain Persistence

Stores sensitive state in the system Keychain:
Keychain Persistence
let persistence = PrismKeychainPersistence()

try persistence.save(credentials, key: "auth_credentials")
let creds: Credentials? = try persistence.load(key: "auth_credentials")

Custom Strategy

Implement PrismPersistenceStrategy for any backend:
Custom Persistence
struct CloudPersistence: PrismPersistenceStrategy {
    func save<State: Codable & Sendable>(_ state: State, key: String) throws {
        let data = try JSONEncoder().encode(state)
        // Upload to your cloud service
    }

    func load<State: Codable & Sendable>(key: String) throws -> State? {
        // Download from your cloud service
        return nil
    }

    func clear(key: String) throws {
        // Delete from your cloud service
    }
}

Time-Travel Debugger

PrismTimeTravelDebugger records state snapshots at every action, letting you navigate through your app’s history:
Time Travel Setup
let debugger = PrismTimeTravelDebugger<AppState>(maxSnapshots: 100)

// Record a snapshot after each action
debugger.record(state: store.state, action: "\(action)")

// Navigate through history
if debugger.canGoBack {
    let snapshot = debugger.goBack()
    store.replaceState(with: snapshot.state)
}

if debugger.canGoForward {
    let snapshot = debugger.goForward()
    store.replaceState(with: snapshot.state)
}

Inspecting Snapshots

Each snapshot captures the state, action description, timestamp, and position:
Snapshot Inspection
for snapshot in debugger.snapshots {
    print("[\(snapshot.index)] \(snapshot.action) at \(snapshot.timestamp)")
}

// Jump to a specific point in history
let snapshot = debugger.goTo(index: 5)
Set maxSnapshots to limit memory usage. Old snapshots are evicted when the limit is exceeded.

Undo / Redo

PrismUndoRedoStack provides a classic undo/redo mechanism:
Undo / Redo
let undoRedo = PrismUndoRedoStack<AppState>(maxStackSize: 50)

// Before mutating state, push the current state
undoRedo.push(store.state)
store.send(.deleteItem(at: index))

// Undo
if undoRedo.canUndo, let previous = undoRedo.undo() {
    store.replaceState(with: previous)
}

// Redo
if undoRedo.canRedo, let next = undoRedo.redo() {
    store.replaceState(with: next)
}
The redo stack is automatically cleared when a new state is pushed, matching standard undo/redo behavior.

Derived Store

PrismDerivedStore provides a read-only view into a parent store that only notifies when the derived value actually changes:
Derived Store
struct AppState: PrismState {
    var users: [User] = []
    var selectedFilter: Filter = .all
}

// Derive a filtered user list
let filteredUsers = store.derive { state in
    state.users.filter { $0.matches(state.selectedFilter) }
}

// Use in SwiftUI
struct UserListView: View {
    let derived: PrismDerivedStore<AppState, [User]>

    var body: some View {
        List(derived.value) { user in
            Text(user.name)
        }
    }
}
PrismDerivedStore uses Equatable on the local state to avoid unnecessary SwiftUI re-renders.

Testing with PrismTestStore

PrismTestStore wraps PrismStore with testing utilities for deterministic assertions:
PrismTestStore
@MainActor
func testIncrement() async {
    let testStore = PrismTestStore(
        initialState: CounterState(),
        reduce: { state, action in
            switch action {
            case .increment: state.count += 1
            case .decrement: state.count -= 1
            }
            return .none
        }
    )

    testStore.send(.increment)
    XCTAssertEqual(testStore.state.count, 1)

    testStore.send(.decrement)
    XCTAssertEqual(testStore.state.count, 0)
}

Testing Async Effects

Use waitForEffects() to wait for in-flight effects to complete before asserting:
Testing Effects
@MainActor
func testFetchUsers() async {
    let testStore = PrismTestStore(
        initialState: UserState(),
        reducer: UserReducer()
    )

    testStore.send(.fetchUsers)
    XCTAssertTrue(testStore.state.isLoading)

    // Wait for the async effect to complete
    try await testStore.waitForEffects()

    XCTAssertFalse(testStore.state.isLoading)
    XCTAssertFalse(testStore.state.users.isEmpty)
}

Testing with Middleware

Testing Middleware
@MainActor
func testAnalyticsMiddleware() async {
    var trackedEvents: [String] = []

    let middleware = PrismSideEffect<AppState, AppAction> { _, action in
        switch action {
        case .purchase:
            return .run { _ in trackedEvents.append("purchase") }
        default:
            return .none
        }
    }

    let testStore = PrismTestStore(
        initialState: AppState(),
        reducer: AppReducer().handling(with: middleware)
    )

    testStore.send(.purchase(item))
    try await testStore.waitForEffects()

    XCTAssertEqual(trackedEvents, ["purchase"])
}

Summary

FeatureTypePurpose
PersistencePrismPersistenceStrategySave/load state across launches
Time TravelPrismTimeTravelDebuggerNavigate through state history
Undo/RedoPrismUndoRedoStackClassic undo/redo with stack
Derived StorePrismDerivedStoreEfficient read-only projections
Test StorePrismTestStoreDeterministic testing utilities