iOS进阶1-combine

前言

Combine 是 Apple 在 WWDC 2019 上推出的一个声明式编程框架,它基于 响应式编程 (Reactive Programming) 思想,用于处理应用中的异步事件流,比如网络请求、数据存储、UI 交互、定时器等。其核心目标是 简化异步代码的编写、提高可读性和可维护性,并提供一种统一的方式来处理各种事件流。

一、Combine 的核心概念

要理解 Combine,首先需要掌握几个核心概念,它们构成了 Combine 编程的基础:

1. Publisher(发布者)
  • 作用:负责产生和发送事件流(数据流或信号)。
  • 特点 :可以发送三种类型的事件:
    • .output(_ value):发送一个数据值(事件流中的正常数据)。
    • .failure(_ error):发送一个错误(事件流终止,后续不再发送数据)。
    • .finished:表示事件流正常结束(后续不再发送数据)。
  • 常见示例
    • NotificationCenter.Publisher:监听系统通知(如网络状态变化、键盘弹出)。
    • URLSession.DataTaskPublisher:发起网络请求并返回数据。
    • Just:发送单个数据后立即结束(用于包装静态数据)。
    • CurrentValueSubject/PassthroughSubject:手动发送事件的发布者(后续详细介绍)。
2. Subscriber(订阅者)
  • 作用:订阅发布者,接收并处理其发送的事件。
  • 特点 :必须实现 receive(subscription:)receive(_ input:)receive(completion:) 三个方法,分别用于处理 "订阅成功"、"接收数据"、"事件流结束(成功 / 失败)"。
  • 常见示例
    • sink:Combine 提供的便捷订阅者,通过闭包处理数据和结束事件(最常用)。
    • assign:将发布者的输出直接赋值给对象的属性(如 UI 控件的 textisHidden 等)。
3. Operator(运算符)
  • 作用:对发布者发送的事件流进行处理(如过滤、转换、合并、延迟等),返回一个新的发布者。
  • 特点:运算符可以链式调用,形成一个 "事件处理管道",让代码逻辑清晰、可组合。
  • 常见分类
    • 过滤类filter(过滤不符合条件的数据)、removeDuplicates(去除重复数据)、compactMap(过滤 nil 并转换类型)。
    • 转换类map(将数据转换为另一种类型)、flatMap(将数据映射为新的发布者,并合并其事件流)、scan(累积数据并返回中间结果)。
    • 合并类merge(合并多个发布者的事件流)、zip(将多个发布者的事件按顺序配对组合)。
    • 时间类delay(延迟发送事件)、throttle(限制事件发送频率)、debounce(防抖,延迟一段时间后发送最后一个事件)。
4. Subscription(订阅关系)
  • 作用:连接发布者和订阅者的桥梁,用于管理订阅的生命周期(如取消订阅)。
  • 特点 :订阅者通过调用 publisher.subscribe(subscriber) 生成一个 Subscription 对象,订阅者可以通过该对象取消订阅(避免内存泄漏)。
  • 自动管理 :Combine 中订阅默认是 "自动取消" 的(当订阅者被释放时,订阅会自动取消),但手动管理订阅(如通过 AnyCancellable 存储订阅)是更安全的实践。
5. Subject(主题)
  • 作用:既是发布者(可发送事件),也是订阅者(可接收其他发布者的事件),用于手动控制事件流的发送。
  • 常见类型
    • PassthroughSubject:只发送 "订阅后" 接收到的事件(不存储历史数据)。
    • CurrentValueSubject:存储最新的一个数据值,当有新订阅者时,会立即发送该最新值(适用于需要 "状态保持" 的场景,如用户登录状态、购物车数据)。

二、Combine 的基本用法(代码示例)

下面通过几个简单的示例,展示 Combine 的核心用法:

示例 1:使用 Justsink 处理静态数据

swift

复制代码
import Combine

// 1. 创建发布者:Just 发送单个数据 "Hello, Combine!" 后结束
let publisher = Just("Hello, Combine!")

// 2. 订阅发布者:使用 sink 处理数据和结束事件
let cancellable = publisher
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("事件流正常结束")
            case .failure(let error):
                print("事件流失败:\(error)")
            }
        },
        receiveValue: { value in
            print("接收到数据:\(value)")
        }
    )

// 输出结果:
// 接收到数据:Hello, Combine!
// 事件流正常结束
示例 2:使用 CurrentValueSubject 管理状态

swift

复制代码
import Combine

// 1. 创建 CurrentValueSubject:存储当前计数(初始值为 0)
let countSubject = CurrentValueSubject<Int, Never>(0)

// 2. 第一次订阅:会立即收到初始值 0
let cancellable1 = countSubject
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { value in
            print("订阅者 1 收到计数:\(value)")
        }
    )

// 3. 发送新事件
countSubject.send(1)
countSubject.send(2)

// 4. 第二次订阅:会立即收到最新值 2
let cancellable2 = countSubject
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { value in
            print("订阅者 2 收到计数:\(value)")
        }
    )

// 5. 发送结束事件
countSubject.send(completion: .finished)

// 输出结果:
// 订阅者 1 收到计数:0
// 订阅者 1 收到计数:1
// 订阅者 1 收到计数:2
// 订阅者 2 收到计数:2
示例 3:使用运算符链式处理事件流

swift

复制代码
import Combine

// 1. 创建发布者:发送 1-10 的整数
let numbersPublisher = (1...10).publisher

// 2. 链式调用运算符:过滤偶数 → 乘以 2 → 取前 3 个数据
let cancellable = numbersPublisher
    .filter { $0 % 2 == 0 } // 过滤偶数:2,4,6,8,10
    .map { $0 * 2 } // 乘以 2:4,8,12,16,20
    .prefix(3) // 取前 3 个:4,8,12
    .sink(
        receiveCompletion: { print("结束:\($0)") },
        receiveValue: { print("处理后的值:\($0)") }
    )

// 输出结果:
// 处理后的值:4
// 处理后的值:8
// 处理后的值:12
// 结束:finished
示例 4:网络请求(URLSession.DataTaskPublisher

swift

复制代码
import Combine
import Foundation

// 1. 定义网络请求的发布者:发起 GET 请求
guard let url = URL(string: "https://api.example.com/data") else { fatalError("无效 URL") }
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { data, response in
        // 转换:验证响应状态码(200-299),并返回数据
        guard let httpResponse = response as? HTTPURLResponse,
              200..<300 ~= httpResponse.statusCode else {
            throw NSError(domain: "网络错误", code: -1, userInfo: nil)
        }
        return data
    }
    .decode(type: [User].self, decoder: JSONDecoder()) // 解码 JSON 数据为 [User] 数组

// 2. 订阅网络请求结果
let cancellable = dataPublisher
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("网络请求失败:\(error)")
            }
        },
        receiveValue: { users in
            print("获取到 \(users.count) 个用户")
        }
    )

// 注:User 需是 Decodable 协议的实现类,用于解析 JSON 数据
struct User: Decodable {
    let id: Int
    let name: String
}

三、Combine 的高级用法

1. Subject 的手动控制

PassthroughSubjectCurrentValueSubject 是手动发送事件的核心工具,适用于 "响应 UI 交互""状态变化通知" 等场景:

swift

复制代码
// 场景:按钮点击事件 → 发送事件流
let buttonTapSubject = PassthroughSubject<Void, Never>()

// 模拟按钮点击(手动发送事件)
buttonTapSubject.send() // 第一次点击
buttonTapSubject.send() // 第二次点击

// 订阅按钮点击事件
let cancellable = buttonTapSubject
    .sink { _ in
        print("按钮被点击")
    }

// 输出:
// 按钮被点击
// 按钮被点击
2. 合并多个发布者

使用 mergezip 合并多个事件流,适用于 "多数据源协同" 场景(如同时获取用户信息和商品列表):

swift

复制代码
// 示例:合并两个发布者的事件流
let publisher1 = Just("A").delay(for: 1, scheduler: RunLoop.main)
let publisher2 = Just("B").delay(for: 2, scheduler: RunLoop.main)

// merge:按事件发送顺序合并(先 A 后 B)
publisher1.merge(with: publisher2)
    .sink { print("merge 结果:\($0)") }

// zip:按索引配对合并((A,B))
publisher1.zip(publisher2)
    .sink { print("zip 结果:\($0)") }

// 输出:
// merge 结果:A
// merge 结果:B
// zip 结果:(A, B)
3. 处理错误

Combine 中错误会终止事件流,需通过 tryMapcatchretry 等运算符处理错误:

swift

复制代码
// 示例:网络请求失败后重试 2 次,失败后返回默认数据
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { data, response in
        // 验证响应状态码
        guard let httpResponse = response as? HTTPURLResponse,
              200..<300 ~= httpResponse.statusCode else {
            throw NSError(domain: "网络错误", code: -1, userInfo: nil)
        }
        return data
    }
    .decode(type: [User].self, decoder: JSONDecoder())
    .retry(2) // 失败后重试 2 次
    .catch { error -> Just<[User]> in
        // 捕获错误,返回默认数据
        print("请求失败,返回默认数据:\(error)")
        return Just([User(id: 0, name: "默认用户")])
    }

// 订阅结果
dataPublisher.sink { users in
    print("最终数据:\(users)")
}
4. 与 SwiftUI 结合

Combine 与 SwiftUI 深度集成,通过 @Published 属性包装器将对象属性转换为发布者,实现 "数据驱动 UI":

swift

复制代码
import SwiftUI
import Combine

// 定义数据模型(ObservableObject + @Published)
class UserViewModel: ObservableObject {
    @Published var username: String = "" // @Published 自动生成发布者
    @Published var isButtonEnabled: Bool = false
    
    // 订阅 username 变化,控制按钮状态
    private var cancellable: AnyCancellable?
    
    init() {
        // 监听 username 变化:长度 >=3 时启用按钮
        cancellable = $username
            .map { $0.count >= 3 }
            .assign(to: \.isButtonEnabled, on: self)
    }
}

// SwiftUI 视图
struct UserView: View {
    @ObservedObject var viewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            TextField("输入用户名", text: $viewModel.username)
                .textFieldStyle(.roundedBorder)
            
            Button("提交") {
                print("用户名:\(viewModel.username)")
            }
            .disabled(!viewModel.isButtonEnabled) // 绑定按钮状态
        }
        .padding()
    }
}

四、Combine 的优势与适用场景

优势:
  1. 统一异步处理:将网络请求、通知、UI 交互等不同类型的异步事件,统一为 "发布者 - 订阅者" 模型,避免回调地狱(Callback Hell)。
  2. 高可组合性:通过运算符链式调用,将复杂逻辑拆分为独立的处理步骤,代码清晰、易维护。
  3. 响应式状态管理CurrentValueSubject@Published 等工具简化了状态变化的通知和管理,尤其适合多组件共享状态(如用户登录状态、全局配置)。
  4. 内存安全 :通过 AnyCancellable 管理订阅生命周期,避免因 "订阅者未释放导致的内存泄漏"。
适用场景:
  • 网络请求(数据获取、提交)。
  • 状态管理(用户信息、购物车、设置项)。
  • UI 交互响应(按钮点击、文本输入、滑动手势)。
  • 事件通知(系统通知、自定义事件)。
  • 数据处理(过滤、转换、合并多数据源)。

五、注意事项

  1. 订阅生命周期管理 :所有订阅必须通过 AnyCancellable 存储(如 var cancellables = Set<AnyCancellable>()),否则订阅会立即取消,无法接收事件。

    swift

    复制代码
    // 正确用法:存储订阅
    var cancellables = Set<AnyCancellable>()
    
    Just("Hello")
        .sink { print($0) }
        .store(in: &cancellables) // 存储到 Set 中
  2. 避免循环引用 :在 sinkassign 等闭包中引用 self 时,需使用 [weak self] 避免循环引用(尤其是在类中)。

    swift

    复制代码
    class MyClass {
        var value: String = ""
        var cancellables = Set<AnyCancellable>()
        
        func setup() {
            Just("Test")
                .sink { [weak self] newValue in
                    self?.value = newValue // 弱引用 self,避免循环引用
                }
                .store(in: &cancellables)
        }
    }
  3. 错误处理 :发布者发送 .failure 事件后会终止,需通过 catchretry 等运算符处理错误,避免事件流意外中断。

  4. 线程调度 :默认情况下,发布者的事件发送和订阅者的处理在同一线程(如网络请求在后台线程),如需切换到主线程(如更新 UI),需使用 receive(on:) 运算符。

    swift

    复制代码
    URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .receive(on: DispatchQueue.main) // 切换到主线程
        .sink { [weak self] data in
            self?.updateUI(with: data) // 主线程更新 UI
        }
        .store(in: &cancellables)

总结

Combine 是 iOS 开发中处理异步事件和状态管理的强大工具,其核心是 "发布者 - 订阅者 - 运算符" 模型。通过 Combine,你可以将复杂的异步逻辑拆分为清晰、可组合的步骤,同时简化状态管理和 UI 响应式更新。掌握 Combine 不仅能提升开发效率,还能让代码更健壮、易维护,是 iOS 进阶开发的必备技能之一。

相关推荐
吴Wu涛涛涛涛涛Tao2 小时前
用 Flutter + BLoC 写一个顺手的涂鸦画板(支持撤销 / 重做 / 橡皮擦 / 保存相册)
android·flutter·ios
MaoJiu3 小时前
Flutter iOS 项目 UIScene 迁移指南
flutter·ios
Antonio9154 小时前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift
私人珍藏库7 小时前
利用 iPhone 或 Apple Watch ,自动锁定和解锁 Windows
ios·iphone
2501_9160088913 小时前
iOS 性能测试的深度实战方法 构建从底层指标到真实场景回放的多工具测试体系
android·ios·小程序·https·uni-app·iphone·webview
kk哥889913 小时前
iOS 26 适配指南:UIScrollView 新特性与最佳实践
macos·ios·cocoa
iOS阿玮14 小时前
屁股坏了,我硬抗了3天,差不多省了几千块这件事儿。
ios
alloc20 小时前
Foundation Models Framework
ios
ajassi20001 天前
开源 Objective-C IOS 应用开发(二十三).a静态库的封装和使用
ios·开源·objective-c