☰
Swift Composable Architecture 核心运行时 Store 完全指南:创建、状态访问、Action 发送与 Store Scoping 实战
2026/10/2 13:35:08 网站建设 项目流程
  • 前端
  • 移动开发

【免费下载链接】swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.

项目地址:https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture
点击查看免费下载

导读

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)值得深挖:

  1. 构建ScopedCore(base:stateKeyPath:actionKeyPath:),在父 core 之上完成状态投影与 action 包装;
  2. 用statekey path 和actioncase key path 组合成ScopeID(Store.swift),作为子 Store 的缓存键;
  3. 通过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.

项目地址:https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture
点击查看免费下载
上一篇:A-to-Z-Resources-for-Students:技术博客流量增长策略
下一篇:A-to-Z-Resources-for-Students:开源许可证详解与选择指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询