SwiftUI 12-Combine 简介

Combine 是 Apple 的响应式编程框架,用于处理异步事件流。本教程涵盖 Combine 的核心概念、常用操作符、内存管理、错误处理以及与 SwiftUI 的集成,适合初学者和进阶开发者。

一、核心概念

1. Publisher:数据发送者

Publisher 发出值(Output)或错误(Failure)。常见实现:

  • Just:发出单一值后完成。
    let publisher = Just("Hello") // 发出 "Hello" 后结束
    
  • Future:异步执行并发出结果。
    let future = Future<String, Error> { promise in
        DispatchQueue.global().async {
            promise(.success("Result")) // 异步成功结果
        }
    }
    
  • Timer.publish:定时发出事件。
    Timer.publish(every: 1.0, on: .main, in: .common) // 每秒发出时间
    
  • PassthroughSubject:手动发送值,无初始值。
    let subject = PassthroughSubject<String, Never>()
    subject.send("Event") // 手动触发事件
    
  • CurrentValueSubject:带当前值的 Publisher。
    let currentValue = CurrentValueSubject<String, Never>("Initial")
    currentValue.value = "New" // 更新值并发送
    

2. Subscriber:数据接收者

Subscriber 处理接收到的值或完成事件。

  • sink:处理值和完成事件。
    publisher.sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion { print("Error: \(error)") }
        },
        receiveValue: { value in print("Value: \(value)") }
    )
    
  • assign(to:on:):绑定值到对象的属性(需为 class 类型)。
    class ViewModel { @Published var text = "" }
    let vm = ViewModel()
    publisher.assign(to: \.text, on: vm) // 绑定到 @Published 属性
    

二、常用操作符

1. 数据转换

  • .map:转换每个值。
    // 将输入值加倍
    [1, 2, 3].publisher.map { $0 * 2 } // 输出: 2, 4, 6
    
  • .flatMap:将值转换为新 Publisher 并合并结果。
    // 每个字母追加 "1"
    ["A", "B"].publisher.flatMap { Just($0 + "1") } // 输出: "A1", "B1"
    
  • .scan:累积计算。
    // 计算累计和
    [1, 2, 3].publisher.scan(0, +) // 输出: 1, 3, 6
    

2. 数据过滤

  • .filter:过滤符合条件的值。
    // 过滤大于 1 的值
    [1, 2, 3].publisher.filter { $0 > 1 } // 输出: 2, 3
    
  • .removeDuplicates:去除重复值。
    // 去除重复字母
    ["A", "A", "B"].publisher.removeDuplicates() // 输出: "A", "B"
    

3. 时间处理

  • .debounce:防抖,延迟处理。
    // 500ms 内无新输入才处理
    $searchText.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
    
  • .throttle:节流,固定间隔处理。
    // 每秒处理最新值
    $buttonTaps.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
    

4. 合并流

  • .merge(with:):合并多个 Publisher 的输出。
    // 合并两个序列
    let p1 = [1, 2].publisher
    let p2 = [3, 4].publisher
    p1.merge(with: p2) // 输出: 1, 2, 3, 4
    
  • .combineLatest:合并最新值。
    // 组合最新值
    let p1 = PassthroughSubject<Int, Never>()
    let p2 = PassthroughSubject<String, Never>()
    p1.combineLatest(p2) // 输出: (1, "A"), (2, "A"), (2, "B")
    p1.send(1); p2.send("A"); p1.send(2); p2.send("B")
    

5. 序列操作

  • .first:取第一个符合条件的值。
    // 取第一个大于 2 的值
    [1, 2, 3, 4].publisher.first(where: { $0 > 2 }) // 输出: 3
    

三、错误处理

  • .catch:捕获错误并替换为新 Publisher。
    // 错误时返回默认值
    publisher.catch { _ in Just("Fallback") }
    
  • .replaceError:用默认值替换错误。
    // 错误时返回 "Default"
    publisher.replaceError(with: "Default")
    
  • .retry:重试失败的 Publisher。
    // 最多重试 3 次
    publisher.retry(3)
    

四、内存管理

  • AnyCancellable:管理订阅生命周期。
    private var cancellables = Set<AnyCancellable>()
    publisher.sink { _ in }.store(in: &cancellables) // 存储订阅
    
  • 手动取消:
    let cancellable = publisher.sink { _ in }
    cancellable.cancel() // 手动取消
    

五、调度与多线程

  • receive(on:):指定接收值的线程。
    // 在主线程接收值
    publisher.receive(on: DispatchQueue.main)
        .sink { value in print("Received: \(value)") }
        .store(in: &cancellables)
    
  • subscribe(on:):指定订阅执行的线程。
    // 在后台线程订阅
    publisher.subscribe(on: DispatchQueue.global())
        .sink { _ in }
        .store(in: &cancellables)
    

六、与 SwiftUI 集成

1. @PublishedObservableObject

自动更新 SwiftUI 视图。

class LoginViewModel: ObservableObject {
    @Published var username = ""
    @Published var password = ""
    @Published var isLoginEnabled = false
    private var cancellables = Set<AnyCancellable>()

    init() {
        // 验证用户名和密码是否有效
        Publishers.CombineLatest($username, $password)
            .map { username, password in
                !username.isEmpty && password.count >= 6
            }
            .assign(to: &$isLoginEnabled)
    }
}

2. SwiftUI 视图

struct LoginView: View {
    @StateObject private var viewModel = LoginViewModel()

    var body: some View {
        Form {
            TextField("Username", text: $viewModel.username)
            SecureField("Password", text: $viewModel.password)
            Button("Login", action: { print("Logging in...") })
                .disabled(!viewModel.isLoginEnabled)
        }
    }
}

七、调试工具

  • print:打印事件流,方便调试。
    // 打印所有事件
    publisher.print("Debug").sink { _ in }.store(in: &cancellables)
    
  • handleEvents:添加副作用(如日志)。
    publisher.handleEvents(receiveOutput: { print("Value: \($0)") })
    

八、常见应用场景

1. 网络请求

func fetchData() -> AnyPublisher<Data, Error> {
    URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com")!)
        .map(\.data)
        .mapError { $0 }
        .eraseToAnyPublisher()
}

2. 防抖搜索

$searchText
    .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
    .removeDuplicates()
    .flatMap { query in
        search(query).replaceError(with: []) // 错误时返回空数组
    }
    .assign(to: \.results, on: self)

总结

类别 核心 API 用途
Publisher Just, Future, PassthroughSubject 创建数据流
Subscriber sink, assign 接收数据
操作符 map, filter, debounce, combineLatest 处理数据流
错误处理 catch, retry, replaceError 错误恢复
内存管理 AnyCancellable, store(in:) 避免内存泄漏
SwiftUI @Published, onReceive, ObservableObject 视图绑定

Combine 提供强大的异步事件处理能力,结合 SwiftUI 可实现响应式 UI 更新。通过合理使用操作符、调度和错误处理,开发者能高效构建健壮的应用。

Demo地址:https://github.com/EvanCaiDev/CombineLearning

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容