TCA Composer 是一个 Swift 宏框架,用于在基于 TCA 的应用程序中生成样板代码。 Composer 提供了一系列 Swift 宏,允许您声明式地构造一个 Reducer
,并自动生成全部或部分的 State
、Action
和 body
声明。 Composer 还可以自动生成整个 Reducer
,用于导航目标和堆栈。 Composer 鼓励使用简单的设计模式来构建您的代码,同时仍然允许您完全灵活地构建代码和应用程序。
重要提示
Composer 需要 Composable Architecture 的 1.7.0 版本(或更高版本)。 Composer 还要求在您的 Reducer
中采用 ObservableState
。 如果您要将现有的 Reducer
迁移到 Composer,强烈建议您首先按照迁移指南更新您的 Reducer
以使用 ObservableState
,以便更平滑地过渡到使用 Composer。
此存储库包含来自 TCA 存储库 的几个已转换为使用 Composer 宏框架的示例,包括
这些示例是开始使用和体验 Composer 的绝佳方式。
让我们从创建典型的 TCA 示例 Counter
开始。 要使用 Composer,请将该软件包添加到您的项目并导入 TCAComposer
模块。 然后,在 Counter
声明中将 @Reducer
宏替换为 @Composer
宏,如下所示。 这就是开始创作所需的一切。 实际上,以下代码已经可以编译。
import ComposableArchitecture
+import TCAComposer
-@Reducer
+@Composer
struct Counter {
}
import ComposableArchitecture
import TCAComposer
@Composer
struct Counter {
+ @ObservableState
+ struct State: Equatable {
+ }
+
+ @CasePathable
+ enum Action {
+ }
+
+ @ReducerBuilder<State, Action>
+ var body: some ReducerOf<Self> {
+ EmptyReducer()
+ }
}
使用 @Composer
宏已经创建了一个功能齐全的 Reducer
。 当然,它现在还什么都不做。 所以让我们改变它。
让我们继续构建 TCA 中常用的简单 Counter 示例。 为此,我们只需添加
@Composer
struct Counter {
struct State {
var count = 0
}
}
@Composer
struct Counter {
+ @ObservableState
struct State {
var count = 0
}
+
+ @CasePathable
+ enum Action {
+ }
+
+ @ReducerBuilder<State, Action>
+ var body: some ReducerOf<Self> {
+ EmptyReducer()
+ }
}
如果缺少 @ObservableState
,Composer 会自动将其应用于您的所有 State
声明。
现在我们已经实现了 State
,让我们实现 Action
。 在这里,使用 Composer 创建 Reducer
开始变得有趣。 Composer 旨在完全负责生成 Reducer
的 Action
。 Composer 鼓励您将 Action
分解为更小的特定于域的动作,而不是在代码中创建一个大的 Action
枚举。 这是 TCA 社区 中使用的常见设计模式。
注意:如果您想自己管理
Action
,您仍然可以使用 Composer。 但是,您需要在Action
中添加一些样板代码才能充分利用 Composer 的功能。 该文档提供了有关如何完成此操作的更多详细信息。
现在,让我们通过创建两个动作 decrementButtonTapped
和 incrementButtonTapped
来实现递增和递减计数的能力。 在普通的 TCA 应用程序中,您将创建一个 Action
枚举并添加两个 case。 在 Composer 中,我们将改为将我们的枚举命名为 ViewAction
。
注意:名称
ViewAction
是按照惯例选择的。 您可以自由选择任何名称,并使用 Composer 以您喜欢的任何方式构建您的代码。 Composer 确实有一些首选的约定,如果您采纳它们,您将获得一些额外的好处,例如自动将@CasePathable
添加到 action 枚举,但您没有义务这样做。
@Composer
struct Counter {
struct State {
var count = 0
}
- enum Action {
+ enum ViewAction {
case decrementButtonTapped
case incrementButtonTapped
}
}
@Composer
struct Counter {
+ @ObservableState
struct State {
var count = 0
}
+ @CasePathable
enum ViewAction {
case decrementButtonTapped
case incrementButtonTapped
}
+
+ @CasePathable
+ enum Action {
+ }
+
+ @ReducerBuilder<State, Action>
+ var body: some ReducerOf<Self> {
+ EmptyReducer()
+ }
}
Composer 已经自动将 @CasePathable
应用于我们的 ViewAction
枚举。 默认情况下,Composer 会自动将 @CasePathable
应用于在您的 reducer 中定义的任何名称中带有 Action
后缀的枚举。 但是,Composer 尚未对 Action
执行任何有趣的操作。 让我们继续创建 body
,看看有什么不同。
通常,Reducer
的所有有趣工作都发生在 body
声明中。 应用程序具有非常大且复杂的 body
声明是很常见的。 与 Action
一样,Composer 鼓励您将 reducer 分解为更小的部分,并完全负责生成 Reducer
的 body
。 为此,Composer 需要您提供一些指导,形式为指令。 指令是附加到 reducer 中部分代码的宏,用于指导 Composer 如何组合 reducer 的 body
和 Action
。 所有指令都以 @Compose...
开头,并且影响 body
组合的指令以 @ComposeBody...
开头。
注意:
@Compose...
指令不生成任何代码,也无法在 XCode 中展开。 它们只是@Composer
宏读取的注释,以确定要生成的代码。
继续我们的 Counter
示例。 我们将把编写 Reducer
的 body
的工作委托给 Composer。 相反,我们将添加一个函数来直接减少 ViewAction
,并且我们将指示 Composer 如何使用 @ComposeBodyActionCase
指令将其组合到我们的 reducer 中。
@Composer
struct Counter {
struct State {
var count = 0
}
- enum Action {
+ enum ViewAction {
case decrementButtonTapped
case incrementButtonTapped
}
}
- var body: some ReducerOf<Self> {
- Reduce { action, state in
+ @ComposeBodyActionCase
+ func view(state: inout State, action: ViewAction) {
switch action {
case .decrementButtonTapped:
state.count -= 1
- return .none
case .incrementButtonTapped:
state.count += 1
- return .none
}
@Composer
struct Counter {
+ @ObservableState
struct State {
var count = 0
}
+ @CasePathable
enum ViewAction {
case decrementButtonTapped
case incrementButtonTapped
}
@ComposeBodyActionCase
func view(state: inout State, action: ViewAction) {
switch action {
case .decrementButtonTapped:
state.count -= 1
case .incrementButtonTapped:
state.count += 1
}
+
+ @CasePathable
+ enum Action: ComposableArchitecture.ViewAction {
+ view(ViewAction)
+ }
+
+ @ReducerBuilder<State, Action>
+ var body: some ReducerOf<Self> {
+ TCAComposer.ReduceAction(\.view) { state, action in
+ self.view(state: &state, action: action)
+ return .none
+ }
+ }
}
Composer 现在已经自动生成了一个功能性的 body
,并在 Action
中添加了一个具有 ViewAction
关联类型的 view
case。 这都归功于 @ComposeBodyActionCase
指令的魔力。 它有两个目的
Action
添加一个 case 成员。view
,将用于 case 名称。@ComposeBodyActionCase("myCustomCaseName")
action
参数的类型将用于 case 的关联值类型。Action
,从而从 body
调用您的函数。state
参数或 Effect
的返回类型。 Composer 将根据签名自动调整它如何调用您的 reduce 函数。 在此示例中,省略了返回类型,Composer 会自动调整为在调用 view
函数后始终返回 .none
effect。Composer 的真正威力来自将子 reducer 组合到父 reducer 中。 让我们创建一个由两个 Counter
reducer 组成的 TwoCounters
reducer。 为了实现这一点,我们将使用附加到我们的顶级 reducer 声明的 @ComposeReducer
宏。 此宏允许您在一个地方声明您的所有子 reducer,包括导航目标和导航堆栈的 reducer。 它还允许一些额外的自定义,例如使 Action
符合 BindableAction
,以便允许从 View
对 State
进行绑定访问。 在下面的示例中,指定了 .bindable
选项给 @ComposeReducer
以启用绑定,并添加了两个名为 counter1
和 counter2
的子项。
@ComposeReducer(
.bindable,
children: [
.reducer("counter1", of: Counter.self, initialState: .init()),
.reducer("counter2", of: Counter.self, initialState: .init())
]
)
@Composer
struct TwoCounters {
struct State {
var isDisplayingSum = false
}
enum ViewAction {
case resetCountersTapped
}
@ComposeBodyActionCase
func view(state: inout State, action: ViewAction) {
switch action {
case .resetCountersTapped:
state.counter1.count = 0
state.counter2.count = 0
}
}
}
@ComposeReducer(
.bindable,
children: [
.reducer("counter1", of: Counter.self, initialState: .init()),
.reducer("counter2", of: Counter.self, initialState: .init())
]
)
@Composer
struct TwoCounters {
+ @_ComposerScopePathable
+ @_ComposedStateMember("counter1", of: Counter.State.self, initialValue: .init())
+ @_ComposedStateMember("counter2", of: Counter.State.self, initialValue: .init())
+ @ObservableState
struct State {
var isDisplayingSum = false
}
+ @CasePathable
enum ViewAction {
case resetCountersTapped
}
@ComposeBodyActionCase
func view(state: inout State, action: ViewAction) {
switch action {
case .resetCountersTapped:
state.counter1.count = 0
state.counter2.count = 0
}
}
+ @CasePathable
+ enum Action: ComposableArchitecture.BindableAction, ComposableArchitecture.ViewAction {
+ case binding(BindingAction<State>)
+ case counter1(Counter.Action)
+ case counter2(Counter.Action)
+ case view(ViewAction)
+ }
+
+ @ComposableArchitecture.ReducerBuilder<Self.State, Self.Action>
+ var body: some ReducerOf<Self> {
+ ComposableArchitecture.BindingReducer()
+ ComposableArchitecture.Scope(state: \.counter1, action: \Action.Cases.counter1) {
+ Counter()
+ }
+ ComposableArchitecture.Scope(state: \.counter2, action: \Action.Cases.counter2) {
+ Counter()
+ }
+ ComposableArchitecture.CombineReducers {
+ TCAComposer.ReduceAction(\Action.Cases.view) { state, action in
+ self.view(state: &state, action: action)
+ return .none
+ }
+ }
+ }
+
+ struct AllComposedScopePaths {
+ var counter1: TCAComposer.ScopePath<TwoCounters.State, Counter.State, TwoCounters.Action, Counter.Action> {
+ get {
+ return TCAComposer.ScopePath(state: \State.counter1, action: \Action.Cases.counter1)
+ }
+ }
+ var counter2: TCAComposer.ScopePath<TwoCounters.State, Counter.State, TwoCounters.Action, Counter.Action> {
+ get {
+ return TCAComposer.ScopePath(state: \State.counter2, action: \Action.Cases.counter2)
+ }
+ }
+ }
}
哇,很多代码! 感谢 .bindable
选项,Composer 自动生成了一个包含 BindingAction
一致性的 Action
。 Action
还包含了我们两个 reducer 子项的 case 以及来自 @ComposeBodyActionCase
宏的 view
action。 自动生成的 body
调用 BindingReducer
,作用域两个子 reducer,然后最终调用我们的 view
函数来 reduce ViewAction
。
您还会注意到,以带有下划线的宏开始的新宏附加到了 State
。 这些是 Composer 用来在您的 Reducer
代码部分中生成代码的内部宏,是 swift 宏系统工作方式的副产品。 内部宏并非供您使用,并且可能会在发布版本之间发生变化。 这是完全展开后 State
的样子
@ObservableState
struct State {
var isDisplayingSum = false
+ static var allComposedScopePaths: AllComposedScopePaths {
+ AllComposedScopePaths()
+ }
+
+ @ObservationStateTracked
+ var counter1: Counter.State = .init()
+ @ObservationStateIgnored
+ private var _counter1: Counter.State
+
+ @ObservationStateTracked
+ var counter2: Counter.State = .init()
+ @ObservationStateIgnored
+ private var _counter2: Counter.State
}
这些宏自动为我们的子 reducer 向 State
添加了新成员,包括对 @ObservableState
的所需支持。 @_ComposerScopePathable
宏与生成的 AllComposedScopePaths
结构相结合,通过为每个子 reducer 生成一个 ScopePath
来改进视图人体工程学,以便您可以使用 store.scopes.counter1
来作用域一个子 reducer,而不是更冗长的 store.scope(state: \.counter1, action: \.counter1)
。 很酷,不是吗?
未来几天将推出更多使用 Composer 的示例。 与此同时,请查看 示例 以了解更复杂的使用方法。
Composer 引入了一个新的 ScopePath
概念,简化了 TCA 应用程序中作用域的创建。 ScopePath
由 Composer 自动创建,并且是一种更简洁的方式,可以使用单个指向 ScopedPath
的 KeyPath
在您的应用程序中作用域 store,而不是单独的状态和动作 KeyPath
。 例如,现在可以编写如下代码
- ChildView(store: store.scope(state: \.child, action: \.child))
+ ChildView(store: store.scopes.child)
- ForEach(store.scope(state: \.children, action: \.children)) {
+ ForEach(store.scopes.children) {
}
- .alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
+ .alert($store.scopes(\.destination.alert))
发行版和 main
的文档可在此处获得
当同一源行上有多个宏时,Xcode 当前不会在源代码编辑器中扩展宏。 当 Composer 向现有的 State
和 Action
声明添加成员时,这是一种常见情况,并且会阻止您看到正在生成的代码。 但是,如果生成的代码产生编译器错误,Xcode 将扩展宏并向您显示该错误。
使用新的 #Preview
表达式宏时,由于宏扩展错误,预览可能在某些情况下无法加载。 当同一源文件中的多个宏引用另一个宏的扩展内容时,这是一个常见问题。 如果您遇到此问题,您可以通过使用 pre-macro PreviewProvider
或将您的 Reducer
定义移动到单独的源文件来解决此问题。
在开发 Composer 时,发现了 Swift 编译器中的许多错误。 其中许多已通过 Composer 的设计和实现中的更改得到缓解。 但是,使用 Composer 时可能仍然会遇到一些编译器问题(尽管在经验中,大多数问题都可以解决)。 如果您遇到麻烦的编译器错误,请提交问题或开始讨论。
特别感谢 Brandon Williams 和 Stephen Celis 在 Point-Free 所做的出色工作,包括 Composable Architecture、CasePaths 和 Swift Macro Testing 项目,这些项目使本项目成为可能。
本库以 MIT 许可证发布。详情请参见 LICENSE 文件。