该库引入了一个类似于 SwiftUI 的 Environment
的 API,用于在 The Composable Architecture (TCA) 中派生和组合 Environment
。
TCA 正在朝着协议 Reducer 发展。这极大地简化了在 feature 之间传递依赖项的方式。 建议迁移到这种方法。 TCA 也将使用 @Dependency
属性包装器和 DependencyKey
协议,因此 您需要执行一些操作,以便在您从该库过渡时,两个系统可以同时工作。
通过 Environment
,人们理解为一种提供依赖项的类型。 该库通过标准化这些依赖项以及在使用 TCA 组合域时,它们从一种环境类型传递到另一种环境类型的方式,从而简化了此过程。 与 SwiftUI 类似,该库允许将值(在本例中为依赖项)向下传递到值树(在本例中为 Reducer 树)中,而无需在每个步骤中都指定它们。 您无需在 Environment
中为依赖项提供初始值,无需将依赖项从父环境注入到子环境,并且在许多情况下,您甚至不需要实例化子环境。
该库带有两个互斥的模块,ComposableEnvironment
和 GlobalEnvironment
,它们为不同的权衡提供了不同的功能。 ComposableEnvironment
允许定义环境,在这些环境中,可以在 Reducer 链中的任何点覆盖依赖项。 与 SwiftUI 类似,为依赖项设置值会向下游传播,直到最终再次被覆盖。 GlobalEnvironment
允许定义全局依赖项,这些依赖项对于链中的所有 Reducer 都是相同的。 这是最常见的配置。 两个模块都在同一个存储库中定义,以保持它们之间的源代码兼容性。
GlobalEnvironment
模块应该适合大多数情况。
我们想要共享的每个依赖项都应该使用 DependencyKey
声明,类似于在 SwiftUI 中使用 EnvironmentKey
声明自定义 EnvironmentValue
的方式。 让我们定义一个 mainQueue
依赖项
struct MainQueueKey: DependencyKey {
static var defaultValue: AnySchedulerOf<DispatchQueue> { .main }
}
此 Key 不需要是公开的。 如果依赖项是存在类型,则甚至可以将其自身用作 DependencyKey
,而无需引入其他类型。
就像我们对 SwiftUI 的 EnvironmentValues
所做的那样,我们也在 Dependencies
中安装它
extension Dependencies {
var mainQueue: AnySchedulerOf<DispatchQueue> {
get { self[MainQueueKey.self] }
set { self[MainQueueKey.self] = newValue }
}
}
无论您使用的是 ComposableEnvironment
还是 GlobalEnvironment
,都有不同的方法来访问您的依赖项。
您可以使用 @Dependency
属性包装器来向您的环境公开依赖项。 此属性包装器将您在 Dependencies
中定义的属性的 KeyPath
作为参数。 例如,要公开上面定义的 mainQueue
,您可以声明
@Dependency(\.mainQueue) var main
请注意,您无需为依赖项提供值。 此属性的有效值是来自环境的当前值,或者如果您未定义,则为 default
值。
您还可以使用 Environment
中的下标直接访问依赖项,而无需公开它。 您将此下标与 Dependencies
中定义的属性的 KeyPath
一起使用。 例如
environment[\.mainQueue]
返回与 @Dependency(\.mainQueue)
相同的值。
您使用哪种方式取决于您。 隐式下标速度更快,但有些人更喜欢使用显式声明来评估环境的依赖项。
当使用 ComposableEnvironment
时,您可以直接通过其在 Dependencies
中的计算属性名称从任何 ComposableEnvironment
子类访问依赖项,即使您未使用 @Dependency
属性包装器公开依赖项
environment.mainQueue
不幸的是,当使用 GlobalEnvironment
时,这种直接访问是不可能的。
定义环境的方式有所不同,这取决于您使用的是 ComposableEnvironment
还是 GlobalEnvironment
。
当使用 ComposableEnvironment
时,您的所有环境都需要是 ComposableEnvironment
的子类。 不幸的是,这是自动处理给定节点上私有环境值状态的存储所必需的。 让我们定义公开 mainQueue
依赖项的 ParentEnvironment
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var main
}
假设您需要将 Child
TCA feature 嵌入到 Parent
feature 中。 您可以使用 @DerivedEnvironment
属性包装器声明嵌入
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var main
@DerivedEnvironment<ChildEnvironment> var child
}
当您访问 ParentEnvironment
的 child
属性时,它会自动继承来自 ParentEnvironment
的依赖项。 您可以使用标准方法 pullback childReducer
childReducer.pullback(state: \.child, action: /ParentAction.child, environment: \.child)
您可以内联地为其声明的子环境赋值,或者让库为您处理实例的初始化。 在最后一种情况下,您甚至可以使用无环境 pullback 嵌入子 Reducer
childReducer.pullback(state: \.child, action: /ParentAction.child)
注意:如果您使用无环境 pullback,则您可能已内联定义的任何初始值都将被丢弃。 在这种情况下,您应该使用标准 pullback,并将 \.child
KeyPath
用作函数 (ParentEnvironment) -> ChildEnvironment
。
当使用 GlobalEnvironment
时,您的环境(无论是值类型还是引用类型)都应符合 GlobalEnvironment
协议。 然后,您可以像使用 ComposableEnvironment
一样定义和使用您的依赖项。 由于所有依赖项都是全局共享的,并且没有特定的依赖项需要继承,因此如果您不使用 @DerivedEnvironment
属性包装器来定义依赖项别名(请参阅下文),则使用它的意义不大。
public struct ParentEnvironment: GlobalEnvironment {
public init() {}
@Dependency(\.mainQueue) var main
}
您仍然可以使用无环境 pullback,使用相同的 API
childReducer.pullback(state: \.child, action: /ParentAction.child)
GlobalEnvironment
的唯一要求是提供 init()
初始化器。 如果您的子环境无法做到这一点,您仍然可以实现 GlobalDependenciesAccessing
标记协议,该协议没有要求,但允许您的类型使用隐式下标访问器访问全局依赖项。 您也可以什么都不做,并使用 @Dependency
,它对其宿主没有像 ComposableEnvironment
版本那样的限制(它需要安装在 ComposableEnvironment
子类中)。 如果您无法符合 GlobalEnvironment
,您只会失去对无环境 pullback 的访问权限。
一旦依赖项被定义为 Dependencies
的计算属性,您就只能通过您的环境访问它们,无论它是 ComposableEnvironment
子类还是符合 GlobalDependenciesAccessing
的某种类型。
要为依赖项设置值,您可以使用来自您的环境的 with(keyPath,value)
链式方法
environment
.with(\.mainQueue, DispatchQueue.main)
.with(\.uuidGenerator, { UUID() })
…
当您使用 GlobalEnvironment
时,每个依赖项都是全局设置的。 如果您两次设置相同的依赖项,则最后一次调用优先。 当您使用 ComposableEnvironment
时,每个依赖项都沿着依赖项树设置,直到最终使用子环境上的 with(keyPath, anotherValue)
调用再次设置它。 这与 SwiftUI Environment
的工作方式相同。
如果同一个依赖项由不同的域使用 Dependencies
中的不同计算属性定义,则可以使用来自您的环境的 aliasing(dependencyKeyPath, to: referenceDependencyKeyPath)
链式方法为它们设置别名。 例如,如果您在某个 feature 中将主队列定义为 .main
,而在另一个 feature 中定义为 mainQueue
,则可以使用以下方式为两者设置别名
environment.aliasing(\.main, to: \.mainQueue)
设置别名后,您可以使用任一 KeyPath
分配值。 如果没有为依赖项设置值,则第二个参数为其 KeyPaths
提供默认值。
您还可以使用 @DerivedEnvironment
属性包装器“当场”为依赖项设置别名。 它的初始化器提供了一个闭包,用于转换提供的 AliasBuilder
。 此类型只有一个链式方法 alias(dependencyKeyPath, to: referenceDependencyKeyPath)
。 例如,如果 main
依赖项在 child
派生环境中定义,则可以使用以下方式定义 ParentEnvironment
中的 mainQueue
依赖项的别名
public class ParentEnvironment: ComposableEnvironment {
@Dependency(\.mainQueue) var mainQueue
@DerivedEnvironment<ChildEnvironment>(aliases: {
$0.alias(\.main, to: \.mainQueue)
}) var child
}
当使用此属性包装器时,您无需使用 .aliasing()
从环境定义别名。
依赖项别名始终是全局的。
在以下情况下,您可以放弃 @DerivedEnvironment
声明
ComposableEnvironment
时,您不需要专门为子环境自定义依赖项。@DerivedEnvironment
属性包装器“当场”为依赖项设置别名。 示例应用程序展示了在使用 ComposableEnvironment
时,此功能如何使用以及如何与属性包装器方法混合使用。当您的环境可以自动实例化时,您可以使用无环境 pullback
childReducer.pullback(state: \.child, action: /ParentAction.child)
// or, for collections of features:
childReducer.forEach(state: \.children, action: /ParentAction.children)
请注意,为了在使用 GlobalEnvironment
时访问此类 pullback,您的环境需要符合 GlobalEnvironment
协议。
根据经验,如果您需要在环境树的中间修改依赖项,则应使用 ComposableEnvironment
。 如果所有依赖项在您的环境中共享,则应使用 GlobalEnvironment
。 由于第一种配置非常罕见,如果您有疑问,我们建议使用 GlobalEnvironment
,因为它是在现有 TCA 项目中实现的最简单的方法。
两种方法之间的主要区别总结在下表中
ComposableEnvironment |
GlobalEnvironment |
|
---|---|---|
环境类型 | 类 | 任何存在类型 (结构体、类等) |
环境树 | 所有节点都应该是ComposableEnvironment 子类 |
自由, 可以在任何时候选择加入/退出 |
依赖项值 | 每个实例可自定义 | 全局定义 |
访问依赖项 | @Dependency 、直接、隐式 |
@Dependency 、隐式 |
为了简化其学习曲线,该库的 API 基于 SwiftUI 的 Environment。 我们有以下功能对应关系
SwiftUI | ComposableEnvironment | 用法 |
---|---|---|
EnvironmentKey |
DependencyKey |
标识共享值 |
EnvironmentValues |
Dependencies |
公开共享值 |
@Environment |
@Dependency |
检索共享值 |
View |
(Composable/Global)Environment |
一个节点 |
View.body |
@DerivedEnvironment 的 |
节点的子节点列表 |
View .environment(keyPath:value:) |
(Composable/Global)Environment .with(keyPath:value:) |
为节点及其子节点设置共享值 |
ComposableEnvironment 的 API 的最新文档可在此处获得:here。
添加
.package(url: "https://github.com/tgrapperon/swift-composable-environment", from: "0.5.0")
到 Package.swift
中的 Package 依赖项,然后
.product(name: "ComposableEnvironment", package: "swift-composable-environment")
// or
.product(name: "GlobalEnvironment", package: "swift-composable-environment")
到您的目标的依赖项,具体取决于您要使用的模块。
当导入最新版本的 TCA 时,使用 @Dependency
属性包装器或 DependencyKey
协议时,很可能会出现歧义。 由于此库将让位于 TCA,因此首选方法如下
import ComposableArchitecture
public typealias DependencyKey = ComposableArchitecture.DependencyKey
public typealias Dependency = ComposableArchitecture.Dependency
由于这些类型别名是在您拥有的模块中定义的,因此在解析类型时,它们将优先于外部定义。
DependencyKey
替换为 Compatible.DependencyKey
,并将 @Dependency
替换为 @Compatible.Dependency
。 您可以使用 Xcode 的查找/替换在所有文件中执行此操作。在这种状态下,您的项目应该可以构建而不会产生歧义。
DependencyKey: TCA's DependencyKey
@Dependency: TCA's @Dependency
---
Compatible.DependencyKey: Composable Environment's DependencyKey
@Compatible.Dependency: Composable Environment's @Dependency
然后,您可以按照自己的节奏迁移到协议 Reducer。 迁移完成后,您可以删除对此库的依赖。 希望它对您有所帮助!