0Pricing
Swift Academy · 课时

副作用与依赖

处理副作用并注入依赖。

副作用与依赖 是 CoddyKit 上的免费 Swift Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Swift Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Swift Academy 课程共包含 4 节课。

效果为何存在

归约器必须保持纯粹:给定状态和操作后,它们只能修改状态。但真实应用需要网络请求和计时器等副作用。TCA 将这些建模为归约器返回的效果,让状态变更保持纯粹,同时将异步工作分开执行。

正是这种分离让功能易于测试。

import ComposableArchitecture

// Reducer stays pure; side effects are values it returns.
// Returning .none means no further work.

返回 .none

当某个操作只改变状态且不需要后续工作时,归约器会返回 .none。这是最常见的效果,表示没有异步工作需要执行。

您之前已经见过它;每个分支最终都可以回退到这个基础效果。

import ComposableArchitecture

Reduce { state, action in
    switch action {
    case .incrementTapped:
        state.count += 1
        return .none
    }
}

使用 Effect.run 执行异步工作

要执行异步工作,您需要返回 .run。它的闭包会接收一个 send 句柄,您可以使用它将结果作为新操作送回系统。

这里,获取操作会加载数据,并在完成后发送响应操作。

import ComposableArchitecture

Reduce { state, action in
    switch action {
    case .reloadTapped:
        return .run { send in
            let value = try await loadNumber()
            await send(.responseReceived(value))
        }
    case let .responseReceived(value):
        state.count = value
        return .none
    }
}

将结果作为操作送回

效果不能直接修改状态,而是通过 send 发送操作。归约器随后处理这些操作并更新状态。这个循环确保所有状态变更都发生在归约器内部。

因此,网络响应会变成一个响应操作,由归约器保存。

import ComposableArchitecture

.run { send in
    let result = try await fetch()
    await send(.dataLoaded(result))
}
// .dataLoaded is then handled by the reducer to set state.

处理效果中的错误

异步工作可能会抛出错误。您可以在效果内部捕获错误并发送失败操作,让归约器平稳地进入错误状态。

.run 上可选的 catch 闭包会接收抛出的错误。

import ComposableArchitecture

.run { send in
    let value = try await loadNumber()
    await send(.responseReceived(value))
} catch: { error, send in
    await send(.loadFailed)
}

依赖为何重要

如果归约器直接调用网络客户端或读取日期,就会变得难以测试和控制。TCA 通过依赖解决了这个问题:这些可注入的服务可以通过 @Dependency 供归约器访问。

在测试中,您可以将它们替换为行为可预测的伪实现。

import ComposableArchitecture

// Instead of calling APIClient.shared directly,
// inject it as a dependency so tests can replace it.

@Dependency 属性包装器

在归约器内部,您可以使用 @Dependency 声明依赖,并通过依赖值中的键路径指定其名称。该库提供了时钟和 UUID 生成器等内置依赖。

这里,归约器引入了连续时钟。

import ComposableArchitecture

@Reducer
struct TimerFeature {
    @ObservableState
    struct State: Equatable { var seconds = 0 }
    enum Action { case start, tick }

    @Dependency(\.continuousClock) var clock

    var body: some ReducerOf<Self> {
        Reduce { state, action in .none }
    }
}

在效果中使用依赖

声明依赖后,您可以在 .run 内部使用它。由于时钟是注入的,测试可以提供一个按需推进时间的测试时钟。

这里,计时器使用注入的时钟每秒跳动一次。

import ComposableArchitecture

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .start:
            return .run { send in
                for await _ in self.clock.timer(interval: .seconds(1)) {
                    await send(.tick)
                }
            }
        case .tick:
            state.seconds += 1
            return .none
        }
    }
}

定义自定义依赖

您可以让某个值遵循 DependencyKey 并提供 liveValue,以注册自定义依赖。然后,您可以向 DependencyValues 添加一个计算属性,使其获得键路径。

这里,一个接口客户端成为一项一等依赖。

import ComposableArchitecture

struct NumberClient {
    var fetch: () async throws -> Int
}

extension NumberClient: DependencyKey {
    static let liveValue = NumberClient {
        try await loadNumber()
    }
}

extension DependencyValues {
    var numberClient: NumberClient {
        get { self[NumberClient.self] }
        set { self[NumberClient.self] = newValue }
    }
}

注入自定义依赖

设置好键路径后,归约器可以使用 @Dependency(\.numberClient) 声明客户端,并在效果中调用它。生产环境使用实时值;测试可以覆盖该值。

这里,重新加载操作通过注入的客户端获取数据。

import ComposableArchitecture

@Reducer
struct NumberFeature {
    @ObservableState
    struct State: Equatable { var value = 0 }
    enum Action { case reload, response(Int) }

    @Dependency(\.numberClient) var numberClient

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .reload:
                return .run { send in
                    let n = try await numberClient.fetch()
                    await send(.response(n))
                }
            case let .response(n):
                state.value = n
                return .none
            }
        }
    }
}

取消副作用

计时器等长时间运行的副作用应该可以取消。您可以为副作用标记一个 cancellable 标识符,之后再从另一个操作中使用 .cancel 取消它。

这样,您就可以按需停止计时器。

import ComposableArchitecture

enum CancelID { case timer }

// Start a cancellable effect:
.run { send in /* ... */ }
    .cancellable(id: CancelID.timer)

// Stop it from another action:
.cancel(id: CancelID.timer)

快速检查:副作用与结果

回顾一下副作用如何将结果传回给归约器。

回顾:副作用与依赖

您已经学习了 TCA 如何处理副作用和依赖注入:

  • 归约器保持纯粹并返回 Effect 值;.none 表示不执行任何工作。
  • .run 执行异步工作,并通过 await send(.action) 将结果传回。
  • 错误通过副作用的 catch 闭包处理。
  • @Dependency 注入时钟或您自己的客户端等服务,这些服务通过 DependencyKey 和 DependencyValues 声明。
  • 可以按标识符让长时间运行的副作用变得可 cancellable。

接下来,您将把多个功能组合在一起,并对其进行全面测试。

import ComposableArchitecture

// Recap: pure reducer + Effect.run for async work
// + @Dependency for testable, injectable services.

常见问题解答

「副作用与依赖」课时是免费的吗?

是的 — 「副作用与依赖」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Swift Academy 课程的其余内容,请升级到 CoddyKit PRO。 Swift Academy 课程共包含 4 节课。

「副作用与依赖」这节课中我会学到什么?

处理副作用并注入依赖。 你通过在浏览器中直接运行的动手代码来练习 Swift Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Swift Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Swift Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「副作用与依赖」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Swift Academy 课中编写并运行代码吗?

能。每节 Swift Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 状态、操作与归约器
  2. Store 与 SwiftUI 集成
  3. 副作用与依赖
  4. 组合和测试功能
← 返回 Swift Academy