一文精通-Combine 框架详解及使用示例

Combine 是 Apple 在 WWDC 2019 推出的一个响应式编程框架,用于处理随时间变化的事件流。它提供了一种声明式的方式来处理异步事件,非常适合处理网络请求、用户界面事件、定时器等场景。

Combine 核心概念

1. Publisher(发布者)

Publisher 是事件的来源,可以发出三种事件:

  • 值(Output)
  • 错误(Failure)
  • 完成(Completion)

2. Subscriber(订阅者)

Subscriber 接收来自 Publisher 的事件,有三种方法:

  • receive(subscription:) - 接收订阅
  • receive(_:) - 接收值
  • receive(completion:) - 接收完成或错误

3. Operator(操作符)

Operator 用于转换、过滤或组合 Publisher 发出的事件流。

基本使用示例

1. 简单 Publisher 和 Subscriber

swift

swift 复制代码
import Combine

// 创建一个简单的 Publisher
let publisher = Just("Hello, Combine!")

// 创建一个 Subscriber
let subscriber = publisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: (error)")
        }
    },
    receiveValue: { value in
        print("Received value: (value)")
    }
)

2. 序列 Publisher

swift

scss 复制代码
let numbers = [1, 2, 3, 4, 5].publisher

numbers
    .sink { number in
        print(number)
    }

3. 使用操作符

swift

swift 复制代码
[1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 }  // 只保留偶数
    .map { $0 * $0 }         // 平方
    .sink { print($0) }      // 输出: 4, 16

常用操作符示例

1. Map

swift

scss 复制代码
[1, 2, 3].publisher
    .map { $0 * 10 }
    .sink { print($0) }  // 输出: 10, 20, 30

2. Filter

swift

scss 复制代码
[1, 2, 3, 4, 5].publisher
    .filter { $0 > 3 }
    .sink { print($0) }  // 输出: 4, 5

3. Reduce

swift

javascript 复制代码
[1, 2, 3, 4].publisher
    .reduce(0) { $0 + $1 }
    .sink { print($0) }  // 输出: 10

4. CombineLatest

swift

scss 复制代码
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()

publisher1
    .combineLatest(publisher2)
    .sink { print("CombineLatest: ($0), ($1)") }

publisher1.send(1)
publisher2.send("A")  // 输出: CombineLatest: 1, A
publisher1.send(2)    // 输出: CombineLatest: 2, A
publisher2.send("B")  // 输出: CombineLatest: 2, B

5. Merge

swift

scss 复制代码
let publisherA = PassthroughSubject<Int, Never>()
let publisherB = PassthroughSubject<Int, Never>()

publisherA
    .merge(with: publisherB)
    .sink { print("Merged: ($0)") }

publisherA.send(1)  // 输出: Merged: 1
publisherB.send(2)  // 输出: Merged: 2
publisherA.send(3)  // 输出: Merged: 3

实际应用示例

1. 网络请求

swift

swift 复制代码
import Combine

struct User: Decodable {
    let name: String
    let email: String
}

func fetchUser() -> AnyPublisher<User, Error> {
    let url = URL(string: "https://api.example.com/user")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(.data)
        .decode(type: User.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

var cancellables = Set<AnyCancellable>()

fetchUser()
    .receive(on: DispatchQueue.main)
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("Error: (error)")
            }
        },
        receiveValue: { user in
            print("User: (user.name), Email: (user.email)")
        }
    )
    .store(in: &cancellables)

2. 用户输入处理

swift

swift 复制代码
import Combine
import UIKit

class SearchViewController: UIViewController {
    @IBOutlet weak var searchField: UITextField!
    @IBOutlet weak var resultsLabel: UILabel!
    
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建搜索文本的 Publisher
        let searchPublisher = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: searchField)
            .map { ($0.object as? UITextField)?.text ?? "" }
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
        
        // 订阅搜索 Publisher
        searchPublisher
            .sink { [weak self] searchText in
                self?.resultsLabel.text = "Searching for: (searchText)"
                // 这里可以触发实际的搜索操作
            }
            .store(in: &cancellables)
    }
}

3. 多个网络请求组合

swift

scss 复制代码
func fetchUserAndPosts(userId: Int) -> AnyPublisher<(User, [Post]), Error> {
    let userPublisher = fetchUser(userId: userId)
    let postsPublisher = fetchPosts(userId: userId)
    
    return userPublisher
        .zip(postsPublisher)
        .eraseToAnyPublisher()
}

fetchUserAndPosts(userId: 123)
    .sink(
        receiveCompletion: { completion in
            if case .failure(let error) = completion {
                print("Error: (error)")
            }
        },
        receiveValue: { (user, posts) in
            print("User: (user.name)")
            print("Posts count: (posts.count)")
        }
    )
    .store(in: &cancellables)

内存管理

Combine 使用 AnyCancellable 来管理订阅的生命周期。当 AnyCancellable 被释放时,订阅会自动取消。

swift

scss 复制代码
var cancellables = Set<AnyCancellable>()

[1, 2, 3].publisher
    .sink { print($0) }
    .store(in: &cancellables)  // 存储订阅以便管理生命周期

错误处理

Combine 提供了多种错误处理操作符:

1. Catch

swift

vbnet 复制代码
func fetchData() -> AnyPublisher<Data, Error> {
    let url = URL(string: "https://api.example.com/data")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(.data)
        .catch { error in
            // 发生错误时返回备用数据
            return Just(Data("Fallback data".utf8))
                .setFailureType(to: Error.self)
        }
        .eraseToAnyPublisher()
}

2. Retry

swift

scss 复制代码
URLSession.shared.dataTaskPublisher(for: url)
    .map(.data)
    .retry(3)  // 失败时重试最多3次
    .sink(...)

调度器(Scheduler)

Combine 允许你指定事件在哪个线程或队列上执行:

swift

scss 复制代码
[1, 2, 3].publisher
    .subscribe(on: DispatchQueue.global())  // 在后台线程执行
    .map { $0 * 2 }                       // 在后台线程执行
    .receive(on: DispatchQueue.main)      // 切换到主线程
    .sink { print($0) }                   // 在主线程执行

自定义 Publisher

swift

swift 复制代码
struct TimerPublisher: Publisher {
    typealias Output = Date
    typealias Failure = Never
    
    let interval: TimeInterval
    
    func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
        let subscription = TimerSubscription(interval: interval, subscriber: subscriber)
        subscriber.receive(subscription: subscription)
    }
}

class TimerSubscription<S: Subscriber>: Subscription where S.Input == Date, S.Failure == Never {
    private var timer: Timer?
    private let interval: TimeInterval
    private var subscriber: S?
    
    init(interval: TimeInterval, subscriber: S) {
        self.interval = interval
        self.subscriber = subscriber
        startTimer()
    }
    
    private func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            _ = self?.subscriber?.receive(Date())
        }
    }
    
    func request(_ demand: Subscribers.Demand) {
        // 这里可以处理背压,但简单实现不需要
    }
    
    func cancel() {
        timer?.invalidate()
        subscriber = nil
    }
}

// 使用自定义 Publisher
let timerPublisher = TimerPublisher(interval: 1)
let cancellable = timerPublisher
    .sink { date in
        print("Current date: (date)")
    }

总结

Combine 是 iOS 开发中强大的响应式编程框架,它:

  1. 提供统一的异步事件处理方式
  2. 通过操作符链式组合,代码更简洁
  3. 内置丰富的操作符满足各种需求
  4. 与 SwiftUI 深度集成
  5. 良好的内存管理和错误处理机制

掌握 Combine 可以显著提高处理异步代码的效率和质量,特别是在复杂的异步操作和数据流处理场景中。

相关推荐
HarderCoder13 分钟前
Swift 6.2 新语法糖:在字符串插值里直接给 Optional 写默认值
swift
在下历飞雨15 分钟前
AI+Kuikly自研DSL初体验:让UI开发更简洁优雅
ios·vibecoding
zzywxc78720 分钟前
深入探讨AI三大领域的核心技术、实践方法以及未来发展趋势,结合具体代码示例、流程图和Prompt工程实践,全面展示AI编程的强大能力。
人工智能·spring·机器学习·ios·prompt·流程图·ai编程
HarderCoder44 分钟前
窥探 `@Observable` 的“小黑盒”:private 属性到底会不会被观察?
swift
zzywxc7871 小时前
AI 在金融、医疗、教育、制造业等领域有着广泛的应用,以下是这些领域的一些落地案例
人工智能·python·spring cloud·金融·swift·空间计算
CocoaKier1 小时前
推荐一个网站,一句话生成网站应用和小程序
前端·ios·ai编程
他们都不看好你,偏偏你最不争气1 小时前
【iOS】 懒加载
ios
HarderCoder1 小时前
Swift 并发避坑指南:自己动手实现“原子”属性与集合
swift
HarderCoder13 小时前
惊!只是 `import Foundation`,`String.contains("")` 的返回值居然变了?
swift
HarderCoder13 小时前
Swift 6.2 新武器:`weak let` —— 既弱引用又不可变的安全魔法
swift