可组合架构(简称 TCA)是一个库,用于以一致且易于理解的方式构建应用程序,并考虑到组合、测试和人体工程学。它可用于 SwiftUI、UIKit 等以及任何 Apple 平台(iOS、macOS、visionOS、tvOS 和 watchOS)。
什么是可组合架构?
了解更多
示例
基本用法
文档
社区
安装
翻译
该库提供了一些核心工具,可用于构建不同目的和复杂性的应用程序。它提供了引人入胜的故事,您可以遵循这些故事来解决构建应用程序时日常遇到的许多问题,例如:
状态管理
如何使用简单的值类型管理应用程序的状态,并在多个屏幕之间共享状态,以便可以立即在另一个屏幕中观察到一个屏幕中的变化。
作品
如何将大型功能分解为较小的组件,这些组件可以提取到自己的独立模块中,并可以轻松地粘合在一起以形成功能。
副作用
如何让应用程序的某些部分以最可测试和最容易理解的方式与外界对话。
测试
如何不仅测试架构中内置的功能,还为由许多部分组成的功能编写集成测试,并编写端到端测试以了解副作用如何影响您的应用程序。这使您能够强有力地保证您的业务逻辑按照您期望的方式运行。
人体工学
如何使用尽可能少的概念和移动部件在一个简单的 API 中完成上述所有任务。
可组合架构是在 Point-Free 的许多剧集中设计的,Point-Free 是一个探索函数式编程和 Swift 语言的视频系列,由 Brandon Williams 和 Stephen Celis 主持。
您可以在这里观看所有剧集,以及从头开始对建筑进行专门的多部分游览。
该存储库附带了大量示例,演示如何使用可组合架构解决常见和复杂的问题。查看此目录以查看全部内容,包括:
案例研究
入门
效果
导航
高阶减速器
可重复使用的组件
地点经理
运动管理器
搜索
语音识别
同步应用程序
井字游戏
托多斯
语音备忘录
寻找更实质性的东西?查看 isowords 的源代码,这是一款基于 SwiftUI 和可组合架构构建的 iOS 单词搜索游戏。
笔记
如需分步交互式教程,请务必查看 Meet the Composable Architecture。
要使用可组合架构构建功能,您需要定义一些对您的域进行建模的类型和值:
State :一种类型,描述您的功能执行其逻辑并呈现其 UI 所需的数据。
Action :表示功能中可能发生的所有操作的类型,例如用户操作、通知、事件源等。
减速器:描述如何将应用程序的当前状态发展到给定操作的下一个状态的函数。减速器还负责返回应该运行的任何效果,例如 API 请求,这可以通过返回Effect
值来完成。
Store :实际驱动您的功能的运行时。您将所有用户操作发送到store,以便store可以运行reducer和effects,并且您可以观察store中的状态变化,以便可以更新UI。
这样做的好处是,您将立即解锁功能的可测试性,并且您将能够将大型、复杂的功能分解为可以粘合在一起的较小域。
作为一个基本示例,请考虑一个显示数字以及用于递增和递减数字的“+”和“−”按钮的 UI。为了让事情变得有趣,假设还有一个按钮,点击该按钮时会发出 API 请求以获取有关该数字的随机事实并将其显示在视图中。
为了实现此功能,我们创建一个新类型来容纳该功能的域和行为,并将使用@Reducer
宏进行注释:
导入 ComposableArchitecture@Reducerstruct 功能 {}
在这里,我们需要为功能的状态定义一个类型,它由当前计数的整数以及表示所呈现事实的可选字符串组成:
@Reducerstruct Feature { @ObservableState struct State:Equatable { var count = 0 var numberFact:字符串? }}
笔记
我们已将@ObservableState
宏应用于State
,以便利用库中的观察工具。
我们还需要为该功能的操作定义一个类型。有一些明显的操作,例如点击递减按钮、递增按钮或事实按钮。但也有一些不太明显的操作,例如当我们收到事实 API 请求的响应时发生的操作:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped caseincrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
然后我们实现body
属性,它负责组成该功能的实际逻辑和行为。在里面我们可以使用Reduce
减速器来描述如何将当前状态改变到下一个状态,以及需要执行什么效果。有些动作不需要执行效果,它们可以返回.none
来表示:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: someReducer{ Reduce { 状态,动作 in 开关动作 { case .decrementButtonTapped: state.count -= 1 返回 .none 情况 .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] 发送 let (data, _) = try wait URLSession.shared.data( 来自:URL(字符串:“http://numbersapi.com/(count)/trivia”)! ) wait send( .numberFactResponse(String(解码: data, as: UTF8.self)) ) } case let .numberFactResponse(fact): state.numberFact = 事实返回.none } } }}
最后我们定义显示该功能的视图。它持有StoreOf
以便它可以观察状态的所有更改并重新渲染,并且我们可以将所有用户操作发送到存储,以便状态更改:
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Increment") { store.send(.incrementButtonTapped) } } 部分 { Button("数字事实") { store.send(.numberFactButtonTapped) } } if let fact = store.numberFact { Text(fact) } } }}
从该存储中驱动 UIKit 控制器也很简单。您可以在viewDidLoad
中观察存储中的状态变化,然后使用存储中的数据填充 UI 组件。该代码比 SwiftUI 版本稍长,因此我们将其折叠在这里:
class FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, bundle: nil) } 需要 init?(coder: NSCoder) { fatalError("init(coder:) 尚未实现") } override func viewDidLoad() { super.viewDidLoad() 让 countLabel = UILabel() 让 decrementButton = UIButton() 让incrementButton = UIButton() 让factLabel = UILabel() // 省略:添加子视图并设置约束... 观察{[弱自我] 守护让自己 否则{返回} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private funcincrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
一旦我们准备好显示此视图(例如在应用程序的入口点中),我们就可以构建一个商店。这可以通过指定启动应用程序的初始状态以及为应用程序供电的减速器来完成:
导入 ComposableArchitecture@mainstruct MyApp: App { var body: 某些场景 { WindowGroup { FeatureView( 商店:商店(初始状态:Feature.State()){Feature()})}}}
这足以让屏幕上有一些东西可以玩。与以普通 SwiftUI 方式执行此操作相比,这肯定要多执行几个步骤,但也有一些好处。它为我们提供了应用状态突变的一致方式,而不是将逻辑分散在某些可观察对象和 UI 组件的各种操作闭包中。它还为我们提供了一种表达副作用的简洁方式。我们可以立即测试这个逻辑,包括效果,而无需做太多额外的工作。
笔记
有关测试的更深入信息,请参阅专门的测试文章。
要进行测试,请使用TestStore
,它可以使用与Store
相同的信息创建,但它会做额外的工作,以允许您断言发送操作时功能如何演变:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
创建测试存储后,我们可以使用它来对整个用户步骤流进行断言。每一步我们都需要证明状态改变了我们的期望。例如,我们可以模拟点击递增和递减按钮的用户流程:
// 测试点击递增/递减按钮是否会更改 countawait store.send(.incrementButtonTapped) { $0.count = 1}等待 store.send(.decrementButtonTapped) { $0.计数 = 0}
此外,如果一个步骤导致执行一个效果,将数据反馈到存储中,我们必须对此进行断言。例如,如果我们模拟用户点击事实按钮,我们期望收到包含事实的事实响应,这将导致填充numberFact
状态:
等待 store.send(.numberFactButtonTapped)等待 store.receive(.numberFactResponse) { $0.numberFact = ???}
然而,我们如何知道什么事实将被发送回给我们呢?
目前,我们的减速器正在使用一种影响现实世界的效果来访问 API 服务器,这意味着我们无法控制其行为。为了编写此测试,我们对互联网连接和 API 服务器的可用性进行了调整。
最好将此依赖项传递给减速器,以便我们在设备上运行应用程序时可以使用实时依赖项,但使用模拟依赖项进行测试。我们可以通过向Feature
缩减器添加一个属性来做到这一点:
@Reducerstruct Feature { let numberFact: (Int) 异步抛出 -> String // ...}
然后我们可以在reduce
实现中使用它:
案例.numberFactButtonTapped: return .run { [count = state.count] 发送 让事实 = 尝试等待 self.numberFact(count) 等待发送(.numberFactResponse(fact)) }
在应用程序的入口点,我们可以提供一个与现实世界 API 服务器实际交互的依赖项版本:
@mainstruct MyApp: App { var body: 一些场景 { WindowGroup { FeatureView( 商店:商店(初始状态:Feature.State()){Feature( numberFact: { let (data, _) = 尝试等待 URLSession.shared.data( 来自:URL(字符串:“http://numbersapi.com/(number)”)! ) return String(解码: 数据, as: UTF8.self) } ) } ) } }}
但在测试中,我们可以使用模拟依赖项,它立即返回确定性的、可预测的事实:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) 是一个很好的数字布伦特" }) }}
通过这一点前期工作,我们可以通过模拟用户点击事实按钮来完成测试,然后接收依赖项的响应以呈现事实:
等待 store.send(.numberFactButtonTapped)等待 store.receive(.numberFactResponse) { $0.numberFact = "0 是一个很好的数字布伦特"}
我们还可以改进在应用程序中使用numberFact
依赖项的人体工程学。随着时间的推移,应用程序可能会发展出许多功能,其中一些功能可能还需要访问numberFact
,并且显式地将其传递到所有层可能会很烦人。您可以遵循一个流程来向库“注册”依赖项,使它们立即可供应用程序中的任何层使用。
笔记
有关依赖关系管理的更深入信息,请参阅专门的依赖关系文章。
我们可以首先将数字事实功能包装在新类型中:
struct NumberFactClient { var fetch: (Int) 异步抛出 -> String}
然后通过使客户端符合DependencyKey
协议来向依赖管理系统注册该类型,该协议要求您指定在模拟器或设备中运行应用程序时要使用的实时值:
扩展 NumberFactClient: DependencyKey { static let liveValue = Self( fetch: { let (data, _) = try wait URLSession.shared .data(from: URL(string: "http://numbersapi.com/(number)")! ) return String(解码: data, as: UTF8.self) } )}extension DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = newValue } }}
完成一点前期工作后,您可以通过使用@Dependency
属性包装器立即开始在任何功能中使用依赖项:
@减速器 struct Feature {- let numberFact: (Int) 异步抛出 -> String+ @Dependency(.numberFact) var numberFact ...- 尝试等待 self.numberFact(count)+ 尝试等待 self.numberFact.fetch(count) }
此代码的工作方式与之前完全相同,但您不再需要在构造该功能的减速器时显式传递依赖项。当在预览、模拟器或设备上运行应用程序时,实时依赖项将提供给减速器,并且在测试中将提供测试依赖项。
这意味着应用程序的入口点不再需要构建依赖项:
@mainstruct MyApp: App { var body: 一些场景 { WindowGroup { FeatureView( 商店:商店(初始状态:Feature.State()){Feature()})}}}
可以在不指定任何依赖项的情况下构建测试存储,但您仍然可以覆盖测试所需的任何依赖项:
let store = TestStore(initialState: Feature.State()) { Feature()} withDependencies: { $0.numberFact.fetch = { "($0) 是一个很好的数字布伦特" }}// ...
这是在可组合架构中构建和测试功能的基础知识。还有很多东西需要探索,比如组合、模块化、适应性、复杂效果等。示例目录有很多项目可供探索,以了解更高级的用法。
版本和main
文档可在此处找到:
main
1.17.0(迁移指南)
1.16.0(迁移指南)
1.15.0(迁移指南)
1.14.0(迁移指南)
1.13.0(迁移指南)
1.12.0(迁移指南)
1.11.0(迁移指南)
1.10.0(迁移指南)
1.9.0(迁移指南)
1.8.0(迁移指南)
1.7.0(迁移指南)
1.6.0(迁移指南)
1.5.0(迁移指南)
1.4.0(迁移指南)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
当您更加熟悉该库时,文档中的许多文章可能会对您有所帮助:
入门
依赖关系
测试
导航
共享状态
表现
并发性
绑定
如果您想讨论可组合架构或对如何使用它来解决特定问题有疑问,您可以在许多地方与其他 Point-Free 爱好者讨论:
对于长篇讨论,我们建议使用此存储库的讨论选项卡。
对于休闲聊天,我们推荐 Point-Free Community slack。
您可以通过将 ComposableArchitecture 添加为包依赖项来将其添加到 Xcode 项目中。
从“文件”菜单中,选择“添加包依赖项...”
在包存储库 URL 文本字段中输入“https://github.com/pointfreeco/swift-composable-architecture”
取决于您的项目的结构:
如果您有一个需要访问该库的应用程序目标,请将ComposableArchitecture直接添加到您的应用程序中。
如果您想从多个 Xcode 目标使用此库,或者混合 Xcode 目标和 SPM 目标,则必须创建一个依赖于ComposableArchitecture的共享框架,然后在所有目标中依赖该框架。有关此示例,请查看 Tic-Tac-Toe 演示应用程序,该应用程序将许多功能拆分为模块,并使用tic-tac-toe Swift 包以这种方式使用静态库。
可组合架构在构建时考虑了可扩展性,并且有许多社区支持的库可用于增强您的应用程序:
可组合架构额外功能:可组合架构的配套库。
TCAComposer:用于在可组合架构中生成样板代码的宏框架。
TCACoordinators:可组合架构中的协调器模式。
如果您想贡献一个库,请打开一个带有链接的 PR!
本自述文件的以下翻译由社区成员贡献:
阿拉伯
法语
印地语
印度尼西亚
意大利语
日本人
韩国人
抛光
葡萄牙语
俄语
简体中文
西班牙语
乌克兰
如果您想贡献翻译,请打开带有要点链接的 PR!
我们有一篇专门的文章,针对人们对图书馆的所有最常见问题和评论。
以下人员在图书馆的早期阶段提供了反馈,并帮助图书馆发展成为今天的样子:
保罗·科尔顿、卡恩·德德奥格鲁、马特·迪普豪斯、约瑟夫·多莱扎尔、艾曼塔斯、马修·约翰逊、乔治·凯马卡斯、尼基塔·列昂诺夫、克里斯托弗·利西奥、杰弗里·马科、亚历杭德罗·马丁内斯、谢·米沙利、威利斯·普卢默、西蒙-皮埃尔·罗伊、贾斯汀·普莱斯、斯文·A·施密特, 凯尔·谢尔曼, 彼得·西玛, 贾斯德夫·辛格, 马克西姆·斯米尔诺夫, 瑞安Stone、Daniel Hollis Tavares 以及所有 Point-Free 订阅者?
特别感谢 Chris Liscio,他帮助我们解决了许多奇怪的 SwiftUI 怪癖,并帮助完善了最终的 API。
感谢 Shai Mishali 和 JointCommunity 项目,我们从中获取了Publishers.Create
的实现,我们在Effect
中使用它来帮助桥接基于委托和回调的 API,从而更轻松地与第 3 方框架进行交互。
可组合架构建立在其他库(特别是 Elm 和 Redux)提出的想法基础上。
Swift 和 iOS 社区中也有很多架构库。其中每一个都有自己的一组优先级和权衡,与可组合架构不同。
肋骨
环形
瑞斯威夫特
工作流程
反应堆套件
接收反馈
莫比乌斯斯威夫特
通量器
PromisedArchitectureKit
该库是在 MIT 许可下发布的。有关详细信息,请参阅许可证。