ComposableEnvironment

Documentation

该库引入了一个类似于 SwiftUI 的 Environment 的 API,用于在 The Composable Architecture (TCA) 中派生和组合 Environment

TCA 正在朝着协议 Reducer 发展。这极大地简化了在 feature 之间传递依赖项的方式。 建议迁移到这种方法。 TCA 也将使用 @Dependency 属性包装器和 DependencyKey 协议,因此 您需要执行一些操作,以便在您从该库过渡时,两个系统可以同时工作。

通过 Environment,人们理解为一种提供依赖项的类型。 该库通过标准化这些依赖项以及在使用 TCA 组合域时,它们从一种环境类型传递到另一种环境类型的方式,从而简化了此过程。 与 SwiftUI 类似,该库允许将值(在本例中为依赖项)向下传递到值树(在本例中为 Reducer 树)中,而无需在每个步骤中都指定它们。 您无需在 Environment 中为依赖项提供初始值,无需将依赖项从父环境注入到子环境,并且在许多情况下,您甚至不需要实例化子环境。

该库带有两个互斥的模块,ComposableEnvironmentGlobalEnvironment,它们为不同的权衡提供了不同的功能。 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 属性包装器

您可以使用 @Dependency 属性包装器来向您的环境公开依赖项。 此属性包装器将您在 Dependencies 中定义的属性的 KeyPath 作为参数。 例如,要公开上面定义的 mainQueue,您可以声明

@Dependency(\.mainQueue) var main

请注意,您无需为依赖项提供值。 此属性的有效值是来自环境的当前值,或者如果您未定义,则为 default 值。

隐式下标

您还可以使用 Environment 中的下标直接访问依赖项,而无需公开它。 您将此下标与 Dependencies 中定义的属性的 KeyPath 一起使用。 例如

environment[\.mainQueue]

返回与 @Dependency(\.mainQueue) 相同的值。

您使用哪种方式取决于您。 隐式下标速度更快,但有些人更喜欢使用显式声明来评估环境的依赖项。

直接访问 (仅限 ComposableEnvironment)

当使用 ComposableEnvironment 时,您可以直接通过其在 Dependencies 中的计算属性名称从任何 ComposableEnvironment 子类访问依赖项,即使您未使用 @Dependency 属性包装器公开依赖项

environment.mainQueue

不幸的是,当使用 GlobalEnvironment 时,这种直接访问是不可能的。

环境

定义环境的方式有所不同,这取决于您使用的是 ComposableEnvironment 还是 GlobalEnvironment

在使用 ComposableEnvironment 时定义环境

当使用 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
}

当您访问 ParentEnvironmentchild 属性时,它会自动继承来自 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 时,您的环境(无论是值类型还是引用类型)都应符合 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() 从环境定义别名。

依赖项别名始终是全局的。

无环境 pullback

省略 @DerivedEnvironment 属性包装器

在以下情况下,您可以放弃 @DerivedEnvironment 声明

使用无环境 pullback

当您的环境可以自动实例化时,您可以使用无环境 pullback

childReducer.pullback(state: \.child, action: /ParentAction.child)
// or, for collections of features:
childReducer.forEach(state: \.children, action: /ParentAction.children)

请注意,为了在使用 GlobalEnvironment 时访问此类 pullback,您的环境需要符合 GlobalEnvironment 协议。

ComposableEnvironmentGlobalEnvironment 之间进行选择

根据经验,如果您需要在环境树的中间修改依赖项,则应使用 ComposableEnvironment。 如果所有依赖项在您的环境中共享,则应使用 GlobalEnvironment。 由于第一种配置非常罕见,如果您有疑问,我们建议使用 GlobalEnvironment,因为它是在现有 TCA 项目中实现的最简单的方法。

两种方法之间的主要区别总结在下表中

ComposableEnvironment GlobalEnvironment
环境类型 任何存在类型
(结构体、类等)
环境树 所有节点都应该是
ComposableEnvironment 子类
自由,
可以在任何时候选择加入/退出
依赖项值 每个实例可自定义 全局定义
访问依赖项 @Dependency、直接、隐式 @Dependency、隐式

与 SwiftUI 的 Environment 的对应关系

为了简化其学习曲线,该库的 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 的协议 Reducer

当导入最新版本的 TCA 时,使用 @Dependency 属性包装器或 DependencyKey 协议时,很可能会出现歧义。 由于此库将让位于 TCA,因此首选方法如下

import ComposableArchitecture
public typealias DependencyKey = ComposableArchitecture.DependencyKey
public typealias Dependency = ComposableArchitecture.Dependency

由于这些类型别名是在您拥有的模块中定义的,因此在解析类型时,它们将优先于外部定义。

在这种状态下,您的项目应该可以构建而不会产生歧义。

DependencyKey: TCA's DependencyKey
@Dependency: TCA's @Dependency
---
Compatible.DependencyKey: Composable Environment's DependencyKey
@Compatible.Dependency: Composable Environment's @Dependency

然后,您可以按照自己的节奏迁移到协议 Reducer。 迁移完成后,您可以删除对此库的依赖。 希望它对您有所帮助!