一文精通-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 可以显著提高处理异步代码的效率和质量,特别是在复杂的异步操作和数据流处理场景中。

相关推荐
归辞...2 小时前
「iOS」————SideTable
macos·ios·cocoa
林大鹏天地7 小时前
iOS 父对象dealloc时触发子对象懒加载导致出现崩溃原因探究和解决
ios
牛巴粉带走10 小时前
Flutter 构建失败:watchOS Target 类型无法识别的解决记录
flutter·ios·apple watch
无知的前端12 小时前
一文读懂 - Swift 和 Objective-C 创建对象时内存分配机制
ios·性能优化·swift
游戏开发爱好者812 小时前
Fiddler中文版使用指南 提升开发流程的一站式抓包与调试体验
android·ios·小程序·https·uni-app·iphone·webview
杂雾无尘12 小时前
分享一个让代码更整洁的 Xcode 开发小技巧:设置文件目标平台
ios·swift·apple
2501_915921431 天前
移动端 WebView 视频无法播放怎么办 媒体控件错误排查与修复指南
android·ios·小程序·https·uni-app·iphone·webview
大熊猫侯佩1 天前
WWDC 25 极地冰原撸码危机:InlineArray 与 Span 的绝地反击
swift·apple·wwdc
AirDroid_cn1 天前
手机防沉迷新招:安卓手机如何成为管理iPhone的遥控器?
android·ios·智能手机·iphone·ipad