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 进阶开发的必备技能之一。

相关推荐
终端安全笔记8 小时前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
97650333511 小时前
iOS 上架 4.3a 被拒【uniapp专讲】
flutter·ios·objective-c·uniapp·swift
黑化旺仔1 天前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan1 天前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo1 天前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记1 天前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程1 天前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne2 天前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift