Swift: Combine基本使用

好的,我们来详细讲解一下 Combine 框架的基本使用。Combine 是 Apple 在 2019 年推出的一个声明式的异步事件处理框架,它使用可组合的函数式转换来处理随时间变化的事件流。它是 SwiftUI 应用中管理状态和异步操作的核心。

核心概念

理解 Combine 的关键在于掌握三个核心概念:

  1. Publisher(发布者)事件的源头。它可以发射三种东西:

    • 值(Values):即你所关心的数据。
    • 完成(Completion):一个成功或失败的事件,表示流已结束。
    • 失败(Failure):一个错误类型,表示流因错误而终止。
    • 一个 Publisher 在没有被订阅前,什么都不会做。
  2. Subscriber(订阅者)事件的接收端。它向 Publisher 发起订阅,并接收来自 Publisher 的事件。常见的 Subscriber 有:

    • sink: 接收值和完成事件。
    • assign: 将接收到的值直接设置到某个对象的属性上。
  3. Operator(操作符)处理事件的工具。它们本身也是 Publisher,接收上游的事件,进行处理后,再发送给下游。通过操作符,你可以过滤、转换、组合事件流。这是 Combine 强大和灵活的关键。

    • 例如:map, filter, flatMap, merge, zip 等。

数据流图Publisher -> (经过零个或多个 Operator) -> Subscriber


基本使用步骤

1. 创建 Publisher

有很多方式可以创建 Publisher:

  • 来自 Foundation :许多现有的 Cocoa API 都提供了 Publisher 版本(通常在 ...Publisher 属性中)。

    • URLSession.dataTaskPublisher(for:): 网络请求。
    • NotificationCenter.default.publisher(for:): 通知。
    • Timer.publish(every:on:in:): 定时器。
  • 使用内置方法

    • Just: 发射一个值然后立即结束。
    • Future: 异步产生一个结果(成功或失败)。
    • Empty/Fail/Deferred: 用于特殊场景。
  • 来自 @Published 属性包装器 (在 SwiftUI 中非常常见):

    swift 复制代码
    class ViewModel {
        @Published var username: String = ""
        // $username 就是一个 Publisher<String, Never>
    }

2. 使用 Operator 进行转换(可选但常见)

在 Publisher 和 Subscriber 之间,你可以链式调用多个操作符。

swift 复制代码
// 假设有一个发射字符串的 publisher
let myPublisher = Just("hello, world")

// 使用操作符转换
let transformedPublisher = myPublisher
    .map { $0.uppercased() } // 转换为大写
    .filter { !$0.isEmpty } // 过滤空字符串

3. 使用 Subscriber 进行订阅和接收

最终,你需要一个 Subscriber 来消费数据流。

使用 sink 订阅: sink 接收两个闭包:一个处理接收到的值,另一个处理完成事件。

swift 复制代码
// 1. 创建一个永远不会失败的整数 Publisher
let publisher = [1, 2, 3, 4, 5].publisher

// 2. & 3. 使用操作符并订阅
let cancellable = publisher
    .filter { $0 % 2 == 0 } // 操作符:只保留偶数
    .map { $0 * $0 }        // 操作符:平方
    .sink(
        receiveCompletion: { completion in
            // 处理完成事件
            switch completion {
            case .finished:
                print("流正常结束")
            case .failure(let error):
                print("流因错误结束: (error)")
            }
        },
        receiveValue: { value in
            // 处理接收到的每一个值
            print("接收到的值: (value)")
        }
    )

// 输出:
// 接收到的值: 4
// 接收到的值: 16
// 流正常结束

使用 assign 订阅: assign 将接收到的值直接赋值给某个对象的某个属性。

swift 复制代码
class MyClass {
    var value: String = "" {
        didSet {
            print("value 被设置为: (value)")
        }
    }
}

let myObject = MyClass()
let publisher = Just("New Value")

// 将 publisher 发出的值赋值给 myObject 的 value 属性
let cancellable = publisher.assign(to: \.value, on: myObject)

// 输出:value 被设置为: New Value

关键点:Cancellable 和内存管理

  • 当你调用 sinkassign 时,返回值是一个 AnyCancellable 对象。
  • 你必须强引用这个对象 。当这个 AnyCancellable 对象被释放(比如离开作用域)时,它会自动取消订阅并释放资源。
  • 如果你不保留它,订阅会立即被取消,你可能什么也收不到。
  • 通常的做法是将所有的 AnyCancellable 收集到一个 `Set`` 中。
swift 复制代码
import Combine

class MyViewModel {
    @Published var currentTime: String = ""
    private var cancellables = Set<AnyCancellable>() // 用于存储订阅

    func setupTimer() {
        // 创建一个每秒发射一次时间的 Publisher
        Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .map { date in
                // 操作符:将 Date 转换为字符串
                let formatter = DateFormatter()
                formatter.timeStyle = .medium
                return formatter.string(from: date)
            }
            .sink { [weak self] newTime in
                // 订阅:将新时间赋值给属性
                self?.currentTime = newTime
            }
            .store(in: &cancellables) // 至关重要!将订阅存储到 Set 中
    }
}

常用操作符示例

  • map : 转换值。

    swift 复制代码
    .map { value in value * 2 }
  • filter : 过滤值。

    swift 复制代码
    .filter { value in value > 10 }
  • flatMap : 将多个 Publisher 扁平化为一个。

    swift 复制代码
    // 假设有一个函数 func fetch(id: Int) -> Publisher<String, Error>
    .flatMap { id in fetch(id: id) }
  • catch : 错误处理,从错误中恢复。

    swift 复制代码
    .catch { error in Just("Default Value") } // 出错时返回一个默认值
  • combineLatest: 组合多个 Publisher,当任何一个发射新值时,发送所有 Publisher 的最新值的元组。

  • merge: 将多个 Publisher 的事件流合并为一个。

  • debounce : 防抖,比如用于搜索框,等待用户停止输入后再发起请求。

    swift 复制代码
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)

总结

  1. 找到源头 : 确定你的数据来自哪个 Publisher(通知、网络请求、定时器、@Published 属性等)。
  2. 组装管道 : 使用 Operator 链式地转换、过滤、组合数据流。
  3. 消费结果 : 在末端使用 sinkassign 来订阅和处理最终的数据或错误。
  4. 管理生命周期务必存储返回的 AnyCancellable 到你的 Set<AnyCancellable> 中,以避免订阅被意外取消。

Combine 的学习曲线稍陡,但一旦掌握,它将成为你处理所有异步和数据流问题的强大而优雅的工具,尤其是在 SwiftUI 开发中。

相关推荐
大熊猫侯佩17 小时前
SwiftUI 三阵诀:杨过绝情谷悟 “视图布阵” 之道
swiftui·swift·apple
大熊猫侯佩17 小时前
斯塔克工业技术日志:用基础模型打造 “战甲级” 结构化 AI 功能
ai编程·swift·apple
HarderCoder2 天前
Swift 数据容器全景手册:Sequence、Collection、Set、Dictionary 一次掌握
swift
HarderCoder2 天前
深入理解 SOLID 原则:用 Swift 编写优雅、可维护的代码
swift
HarderCoder2 天前
Swift 并发全景指南:Thread、Concurrency、Parallelism 一次搞懂
swift
HarderCoder2 天前
Swift 并发模型深度解析:Singleton 与 Global Actor 如何抉择?
swift
HarderCoder2 天前
Swift Global Actor 完全指南
swift
HarderCoder2 天前
Swift 计算属性(Computed Property)详解:原理、性能与实战
swift
HarderCoder2 天前
Swift Property Wrapper:优雅地消除样板代码
swift