- 前端
- 移动开发
【免费下载链接】swift-composable-architecture
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
导读
Store是 Swift Composable Architecture(TCA)中驱动整个应用运行的运行时对象,也是视图与业务逻辑之间的唯一桥梁。本指南以官方文档 Store.md 为主线,结合 Store.swift 源码与相关观察、绑定扩展,系统讲解 Store 的创建方式、状态读取、Action 发送、Store 作用域切分(scoping)、Binding 绑定以及 Combine 集成。读完本指南,你将掌握如何在一个真实 TCA 项目中正确地构造根 Store、向子视图派生子 Store、绑定可写状态,并理解 StoreTask 生命周期管理与底层缓存机制。
Store 在 TCA 架构中的定位
从源码注释(Store.swift)可以看到,Store 被明确定义为"驱动应用的运行时(runtime)",是需要在视图中传递、用于读取功能状态(state)和发送用户操作(action)的对象。它的类声明为:
@dynamicMemberLookup @preconcurrency @MainActor public final class Store<State, Action>: _Store几个关键设计点:
@MainActor:Store 的所有操作都限定在主线程执行,这是 UI 框架安全性的硬性保证;@dynamicMemberLookup:允许视图直接通过store.count这样的语法读取状态属性;final class:Store 是引用类型,应用通常只创建一个根 Store,再通过scope派生多个子 Store。
一个典型的应用入口会在App中持有根 Store,并在WindowGroup中传递给根视图:
@main struct MyApp: App { static let store = Store(initialState: AppFeature.State()) { AppFeature() } var body: some Scene { WindowGroup { RootView(store: Self.store) } } }注意:Xcode 预览运行时同样会创建 App 入口,导致 Store 及依赖被提前执行,因此源码特别建议将根 Store 保存在
static let中(见 Store.swift),避免预览场景下的意外副作用。
创建 Store:init(initialState:reducer:withDependencies:)与StoreOf
初始化方法
Store 的公开初始化方法签名如下(Store.swift):
public convenience init<R: Reducer<State, Action>>( initialState: @autoclosure () -> R.State, @ReducerBuilder<State, Action> reducer: () -> R, withDependencies prepareDependencies: ((inout DependencyValues) -> Void)? = nil )三个参数的作用:
| 参数 | 类型 | 说明 |
|---|---|---|
initialState | 自动闭包 | 应用启动时的初始状态,使用@autoclosure保证惰性求值 |
reducer | @ReducerBuilder | 驱动业务逻辑的 Reducer(可由Reduce、.onChange、.ifLet等操作符组合构建) |
withDependencies | 可选闭包 | 覆盖依赖注入容器中的依赖,nil时使用默认依赖 |
底层实现(Store.swift)在初始化时会做三件事:读取当前依赖注入环境、为当前状态追加一个新的导航 ID(navigationIDPath.append(NavigationID()))、最后将固定后的依赖写回 reducer 的dependency(\.self, dependencies)。这意味着创建 Store 的时刻就是依赖固化(snapshot)的时刻,后续依赖变化不会影响该 Store。
实际使用示例:
let store = Store( initialState: AppFeature.State(), reducer: { AppFeature() }, withDependencies: { $0.apiClient = .mock } )StoreOf便捷别名
Store需要两个泛型参数State与Action,而一个 Reducer 的领域通常固定为R.State和R.Action。因此源码定义了一个类型别名(Store.swift):
public typealias StoreOf<R: Reducer> = Store<R.State, R.Action>对比两种写法:
// 完整写法 let store: Store<Feature.State, Feature.Action> // StoreOf 写法 let store: StoreOf<Feature>StoreOf大量出现在视图属性声明中,例如let store: StoreOf<AppFeature>,不仅减少样板代码,还让 Reducer 与 Store 的领域自动保持同步。
访问状态:state、动态成员查找与withState(_:)
直接读取state
当State符合ObservableState协议(即状态标注了@ObservableState宏)时,Store 暴露只读的state属性(Store+Observation.swift):
public var state: State { self.observableState }其中observableState会先通过 observation registrar 登记对该状态的访问,再返回当前状态——这正是视图内读取store.state能够被 Observation 框架跟踪依赖的原因。
动态成员查找
得益于@dynamicMemberLookup,Store 上定义了读下标(Store+Observation.swift):
public subscript<Value>(dynamicMember keyPath: KeyPath<State, Value>) -> Value { self.state[keyPath: keyPath] }这使得视图可以直接写:
struct RootView: View { let store: StoreOf<AppFeature> var body: some View { Form { Text("\(store.count)") // 等价于 store.state.count Button("Tap") { store.send(.buttonTapped) } } } }withState(_:)已废弃
早期版本通过withState读取状态,但现在它被标记为废弃(Deprecations.swift),官方迁移建议是改用@ObservableState宏:
// 旧写法(已废弃) store.withState { $0.count } // 新写法 store.count详细迁移步骤可参考 MigratingTo1.7.md。
发送 Action:send(_:)系列与StoreTask
基本发送
send是视图与业务逻辑交互的唯一入口:
@discardableResult public func send(_ action: Action) -> StoreTask标注@discardableResult意味着可以忽略返回值;内部实现会调用底层 core 的send(action, origin: .store)(Store.swift)。
带动画与事务的发送
文档列出另外两个重载:
send(_:animation:):携带 SwiftUI 动画发送 action;send(_:transaction:):携带完整Transaction发送。
不过源码中这两个 API 已被标注为"计划废弃"(deprecation 年份设为 9999),推荐的新写法是:
// 旧写法(计划废弃) store.send(.increment, animation: .default) // 推荐写法 withAnimation(.default) { store.send(.increment) }对于事务同理:withTransaction(transaction) { store.send(action) }(见 Store.swift 与 #L236-L270)。在 Effect 内部发送带动画的 action 时,应使用.run { send in await send(.response, animation: .default) }(见 Deprecations.swift)。
StoreTask:效果生命周期与取消
send返回的StoreTask代表该 action 触发的 Effect 的生命周期(Store.swift),它提供三个成员:
| 成员 | 类型 | 作用 |
|---|---|---|
cancel() | 方法 | 取消底层任务 |
finish() | async 方法 | 等待任务执行完毕 |
isCancelled | 只读属性 | 任务是否已被取消 |
典型用法是把效果的生命周期绑定到 SwiftUI 的task视图修饰符上:
.task { await store.send(.task).finish() }当视图离开屏幕时,task修饰符自动取消异步上下文,StoreTask会随之取消对应的 Effect——这正是 TCA 中效果自动清理的核心机制。与 Swift 原生Task不同,StoreTask会在当前异步上下文与任务之间自动建立取消处理(Store.swift)。
派生子 Store:scope(_:action:)与作用域切分
为什么需要 scope
大型应用中,根 Store 承载整个应用的领域(State + Action)。如果把根 Store 直接传给每个子视图,子视图将被迫依赖全局领域,模块化将无从谈起。scope方法允许把根 Store 变换为只处理某个子领域(child state/child action)的 Store:
public func scope<ChildState, ChildAction>( _ state: KeyPath<State, ChildState>, action: CaseKeyPath<Action, ChildAction> ) -> Store<ChildState, ChildAction>例如一个包含登录、搜索、个人页三个 Tab 的应用:
struct AppView: View { let store: StoreOf<AppFeature> var body: some View { TabView { ActivityView(store: store.scope(\.activity, action: \.activity)) .tabItem { Text("Activity") } SearchView(store: store.scope(\.search, action: \.search)) .tabItem { Text("Search") } ProfileView(store: store.scope(\.profile, action: \.profile)) .tabItem { Text("Profile") } } } }通过 scoping,SearchView可以被打包成独立模块,完全不感知AppFeature.State与AppFeature.Action的存在(Store.swift)。
底层实现:ScopedCore 与子 Store 缓存
scope的实现(Store.swift)值得深挖:
- 构建
ScopedCore(base:stateKeyPath:actionKeyPath:),在父 core 之上完成状态投影与 action 包装; - 用
statekey path 和actioncase key path 组合成ScopeID(Store.swift),作为子 Store 的缓存键; - 通过
children字典缓存子 Store(Store.swift):相同 key path 组合重复调用 scope 会返回同一个 Store 实例,避免视图刷新时不断创建新 Store 导致状态丢失。
当子状态是可选值(Optional)时,还有对应的 optional 版本scope(_:action:fileID:filePath:line:column:),它返回Store<ChildState, ChildAction>?,常用于if let解包子 Store:
if let childStore = store.scope(\.child, action: \.child) { ChildView(store: childStore) }重要:此操作只能在 SwiftUI 视图内部或
withPerceptionTracking中使用,才能正确观察可选状态的变化(Store+Observation.swift)。文件路径、行号参数用于定位运行时警告,自动由#fileID、#line等字面量填充。
可写绑定:动态成员下标与BindableAction
当 Store 的Action符合BindableAction(且State与Action.State一致)时,Store 额外获得可写的动态成员下标(Binding+Observation.swift):
public subscript<Value: Equatable & Sendable>( dynamicMember keyPath: WritableKeyPath<State, Value> ) -> Value { get { self.state[keyPath: keyPath] } set { self.send(.set(keyPath.unsafeSendable(), newValue, ...)) } }读取返回当前状态,写入则通过发送.set绑定 action 走完整 reducer 管道。于是视图可以极简地绑定可写字段:
TextField("Search", text: $store.query)这里的$store.query通过Binding(subscript:)桥接,将 SwiftUI 的Binding与 Store 的可写下标打通。若Action符合ViewAction(且其ViewAction符合BindableAction),同样支持通过.view(.set(...))间接绑定(Binding+Observation.swift)。
Combine 集成:StorePublisher
对于仍在使用 Combine 的场景,Store 提供publisher属性(Store.swift),返回类型为StorePublisher<State>——一个@dynamicMemberLookup的 Publisher,其Output = State、Failure = Never。
它支持动态成员查找,可直接抽取状态中的某个字段,且该字段必须Equatable,以便自动removeDuplicates()(Store.swift):
store.publisher.alert .sink { ... }需要注意:StorePublisher 目前已被标注为计划废弃,官方建议改用 Observation 框架(@ObservableState+observe)替代,但在存量 Combine 代码中仍可正常使用。
ObservableObject 与 Observation:Store 的观察方式
Store 声明为ObservableObject,但这一协议符合是"惰性"的(Store.swift):它的唯一用途是让 Store 能被 SwiftUI 的@StateObject持有,而不是通过@ObservedObject观察。
extension Store: ObservableObject {}真正的状态观察依赖两套机制:
- iOS 17+(或 macOS 14 等新平台):Store 在
canImport(Observation)条件下扩展为Observable(Store.swift),配合状态上的@ObservableState宏使用; - 旧系统(iOS < 17):通过 Perception 包实现等价的感知跟踪,Store 扩展为
Perceptible(Store+Observation.swift)。
此外 Store 还实现了值语义比较:Equatable采用引用相等(===),Hashable基于对象标识,并符合Identifiable(Store+Observation.swift),这使得 Store 可以直接放入 SwiftUI 的ForEach、task等场景使用。
其他值得了解的接口与弃用接口
Store 文档中还列出了以下补充接口:
- scope 的另一种形式:早期带参数标签的
scope(state:action:)已被renamed: "scope(_:action:)"标记弃用(Store.swift); case运算符:与 case key path 模式匹配相关的运算符,用于对 Store 的 action 做 case 分解与绑定;- 调试描述:Store 的
debugDescription会智能输出类型名,例如当State/Action恰好以.State/.Action结尾且前缀一致时输出StoreOf<Feature>(Store.swift),方便日志与调试; - 生命周期日志:Store 初始化与析构都会写入统一 Logger(Store.swift),结合
storeTypeName可以追踪 Store 的创建与释放,排查内存泄漏。
所有已弃用接口的完整说明集中收录在 StoreDeprecations.md 中。
测试验证与进一步探索
Store 的核心行为在仓库中有大量测试覆盖,可作为理解与验证的参考:
- StoreTests.swift:Store 初始化、send、scope 的基础行为;
- StorePerceptionTests.swift:Observation/Perception 感知跟踪的正确性;
- BindableStoreTests.swift:可写绑定下标的读写与 reducer 联动;
- StoreLifetimeTests.swift:Store 生命周期、子 Store 缓存与释放行为。
在实际项目中,建议遵循"一个根 Store + 按功能 scope 子 Store"的实践:根 Store 只在 App 入口创建一次,子视图一律通过store.scope(...)接收最小领域的 Store;需要写回状态时优先使用@ObservableState配合可写动态成员下标,让绑定始终经过 reducer,从而保证整个应用的状态流转可预测、可测试。
- 前端
- 移动开发
【免费下载链接】swift-composable-architecture
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
相关推荐
深入Zustand核心:Store创建与状态管理
深入Zustand核心:Store创建与状态管理 本文深入探讨了Zustand状态管理库的核心机制,从create函数的工作原理与实现机制开始,详细解析了其函数
前端Pinia 组合式 Store 实战:跨 Store 共享状态、Getter 与 Action 的完整指南
Pinia 组合式 Store 实战:跨 Store 共享状态、Getter 与 Action 的完整指南 组合式 Store(Composing Stores
前端状态管理Yup 扩展指南:通过 addMethod、transform 与继承构建自定义 Schema 校验能力
Yup 扩展指南:通过 addMethod、transform 与继承构建自定义 Schema 校验能力 Yup 是一个用于运行时值解析与校验的 schema
前端移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考