AgentSkillsCN

composestore

当用户提出“访问日志数据”“读取日志状态”“修改日志”“使用日志提供商”“关注日志”“创建日志功能”“添加日志字段”“保存日志”“更新日志”“删除日志”,或在本代码库中寻求关于Riverpod提供者模式在日志CRUD操作中的指导时,应使用此技能。

SKILL.md
--- frontmatter
name: composestore
description: "ComposeStore Swift state management library guide. Use when writing Store subclasses, defining State structs, using @Intercept/@BaseInterceptors macros, implementing interceptors (StoreInterceptor), handling async state with Async<T>, integrating with SwiftUI/UIKit/AppKit, or when the user mentions ComposeStore, Store, commit, dispatch, or interceptor in a Swift context."

ComposeStore

Lightweight Swift state management built on @Observable. Provides Store<S> base class with commit/dispatch mutations, NestJS-style interceptor macros, and Async<T> for loading states.

Quick Start

swift
import ComposeStore

// 1. Define State (struct + Equatable recommended)
struct Counter: State, Equatable {
    var count: Int = 0
}

// 2. Create Store
@MainActor @Observable
final class CounterStore: Store<Counter> {
    init() { super.init(state: Counter()) }

    func increment() {
        commit { state in state.count += 1 }
    }

    func loadData() async {
        commit { state in state.count = 0 }  // reset
        let value = await fetchFromAPI()
        commit { state in state.count = value }
    }
}

// 3. Use in SwiftUI
struct ContentView: View {
    @SwiftUI.State private var store = CounterStore()
    var body: some View {
        Text("\(store.state.count)")
        Button("Increment") { store.increment() }
    }
}

Core API

State Protocol

swift
public protocol State: Sendable {}

Implement as struct conforming to Equatable for efficient change detection. Without Equatable, computed(new:old:) fires on every mutation.

Store<S: State>

@MainActor @Observable open class Store<S: State>

Mutation methods:

MethodUsage
commit { state in ... }Inline state mutation
commit(mutation:payload:)Named mutation with payload
dispatch(action:payload:) asyncAsync action

Lifecycle:

MethodPurpose
computed(new:old:)Override for derived values after mutation
interceptorsOverride or use @BaseInterceptors for base interceptor chain

Async<T>

Unified async state enum: .idle, .loading, .value(value: T), .error(value: Error?)

swift
struct MyState: State, Equatable {
    var users: Async<[User]> = .idle
}
// Usage: commit { s in s.users = .loading }

Interceptor System

NestJS-style interceptor chain wrapping Store method calls. See references/interceptors.md for full guide.

Minimal Example

swift
// Interceptor with parameterless init (required for macros)
@MainActor
struct LoggingInterceptor: StoreInterceptor {
    func intercept(context: ExecutionContext, next: @escaping () -> any State) -> any State {
        print(">> \(context.methodName)")
        let result = next()
        print("<< \(context.methodName)")
        return result
    }
}

@BaseInterceptors(LoggingInterceptor.self)
@MainActor @Observable
final class MyStore: Store<Counter> {
    init() { super.init(state: Counter()) }

    // Logging -> body
    @Intercept
    func increment() {
        self.commit { state in state.count += 1 }  // self. REQUIRED
    }
}

stopChain() — Skip Remaining Interceptors

context.stopChain() skips downstream interceptors but always executes the body. Use for caching, error handling, or conditional bypass.

swift
func intercept(context: ExecutionContext, next: @escaping () -> any State) -> any State {
    if let cached = Self.cache[context.methodName] {
        context.payload[Self.cachedData] = cached
        return context.stopChain()  // skip remaining interceptors, run body
    }
    return next()  // cache miss → continue chain normally
}
  • context.isStopped — readable by parent interceptors after next() returns
  • Async version: return await context.stopChain()
  • See references/interceptors.md for full guide

Critical: self. Requirement in @Intercept

BodyMacro path (methods WITHOUT context: ExecutionContext): The @Intercept macro wraps the body in an @escaping closure. Use explicit self. for all member access.

swift
// CORRECT
@Intercept
func increment() {
    self.commit { state in state.count += 1 }
}

// WRONG - compile error in Xcode
@Intercept
func increment() {
    commit { state in state.count += 1 }  // Missing self.
}

PeerMacro path (methods WITH context: ExecutionContext): Body is NOT wrapped. No self. needed.

swift
// CORRECT - no self. needed
@Intercept(FetchEffect.self)
private func fetchData(context: ExecutionContext) async {
    let data = context.payload[FetchEffect.result] ?? []
    commit { state in state.data = data }  // self. optional
}

When to Use Which Path

PatternWhenself. needed?
@Intercept (no context)Simple sync/async mutationsYes
@Intercept + context: ExecutionContextSide-effect separationNo

UIKit / AppKit Integration

Use StoreObserver for imperative UI frameworks. See references/uikit-appkit.md.

swift
observer = StoreObserver { [weak self] in
    guard let self else { return }
    self.label.text = "\(self.store.state.count)"
}
// In deinit: observer?.cancel()

References

  • Interceptor System — Full interceptor guide: @BaseInterceptors, @Intercept, PeerMacro pattern, PayloadKey, side-effect separation
  • UIKit/AppKit Integration — StoreObserver, observeStore helper
  • Examples — Complete Store examples with SwiftUI, interceptors, async patterns