iOS Combine 响应式框架
iOS Combine 响应式框架
摘要:Combine 是 Apple 官方的响应式编程框架,提供 Publisher/Operator/Subscriber 三件套,统一处理异步事件流。本文讲解核心概念、常用操作符、与 SwiftUI 集成及实战模式。
一、Combine 解决什么问题
iOS 开发中异步来源繁多:网络回调、KVO、通知中心、按钮事件、定时器。每种 API 形态各异,组合起来容易写出嵌套回调。Combine 用统一的"事件流"抽象把它们统一起来,并提供丰富的操作符做转换、过滤、合并,代码更声明式、更易测试。
二、核心三件套
Publisher
发布事件,可发射三种信号:值(Output)、成功结束(Finished)、失败(Failure)。
let publisher = Just(42) // 发射一个值后结束
let range = (1...5).publisher // 依次发射 1~5
let fail = Fail<Int, Error>(MyError.x)
Subscriber
订阅 Publisher,处理事件:
publisher.sink(
receiveCompletion: { completion in print("结束: \(completion)") },
receiveValue: { value in print("值: \(value)") }
)
sink 返回 AnyCancellable,必须持有否则订阅立即取消。
Operator
在中间做转换:
publisher
.map { $0 * 2 }
.filter { $0 > 4 }
.sink { print($0) }
三、Subject
手动注入事件的桥:
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)
subject.send(completion: .finished)
PassthroughSubject:无缓冲,新订阅者拿不到历史值。CurrentValueSubject:保留当前值,新订阅者立即收到。
四、常用操作符
转换
publisher.map { $0 * 2 }
publisher.tryMap { try parse($0) }
publisher.scan(0) { acc, v in acc + v } // 累加
过滤
publisher.filter { $0 > 0 }
publisher.removeDuplicates()
publisher.compactMap { $0 as? String }
时间
publisher.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
publisher.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
publisher.collect(.byTime(RunLoop.main, .seconds(1))) // 按时间窗口聚合
组合
Publishers.Zip(a, b) // 一对一配对
Publishers.Merge(a, b) // 交错合并
Publishers.CombineLatest(a, b) // 任一变化都发射最新组合
错误处理
publisher
.catch { error in Just(fallbackValue) } // 出错时切换到备用流
.retry(3) // 失败重试最多 3 次
五、调度与线程
publisher
.receive(on: DispatchQueue.global())
.map { heavy($0) }
.receive(on: DispatchQueue.main)
.sink { updateUI($0) }
receive(on:):切换后续操作执行的队列。subscribe(on:):指定订阅本身发生的队列。
六、网络请求示例
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: User.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
fetchUser(id: 1)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })
.store(in: &cancellables)
dataTaskPublisher 把网络请求变成 Publisher,链式解码、切线程、订阅,一气呵成。
七、与 SwiftUI 集成
@StateObject + assign
class Store: ObservableObject {
@Published var count = 0
private var cancellables = Set<AnyCancellable>()
init() {
Timer.publish(every: 1, on: .main)
.autoconnect()
.scan(0) { acc, _ in acc + 1 }
.assign(to: \.count, on: self)
.store(in: &cancellables)
}
}
onReceive
struct View: SwiftUI.View {
@State private var text = ""
var body: some View {
Text(text)
.onReceive(NotificationCenter.default.publisher(for: .dataUpdated)) { note in
text = note.userInfo?["value"] as? String ?? ""
}
}
}
八、自定义 Publisher
实现 Publisher 协议:
struct MyPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
let subscription = MySubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
多数场景用 Deferred + PassthroughSubject 包装现有 API 即可,不必从头实现。
九、背压与缓冲
Combine 默认无限缓冲,订阅者跟不上时值会堆积。可用:
publisher.buffer(size: 16, prefetch: .keepFull, whenFull: .dropOldest)
控制缓冲策略,避免内存膨胀。
十、测试
用 CombineTest 思路:把订阅结果收集到数组断言。
var output: [Int] = []
let cancellable = publisher.sink { output.append($0) }
XCTAssertEqual(output, [2, 4, 6])
更专业可用 CombineExpectations 库,提供 RecordingExpectation 等工具。
十一、实战模式
搜索框防抖
searchSubject
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { query in fetchSearch(query).catch { _ in Just([]) } }
.receive(on: DispatchQueue.main)
.assign(to: \.results, on: self)
.store(in: &cancellables)
多请求并发聚合
Publishers.Zip3(fetchUser(), fetchOrders(), fetchCoupons())
.map { (user, orders, coupons) in Profile(user, orders, coupons) }
.sink { ... }
任一失败整体失败;若想部分失败容错,用 catch 把每个流降级为空再 Merge。
十二、总结
Combine 把 iOS 中分散的异步源统一为事件流,配合丰富的操作符可以写出声明式、可组合、易测试的代码。掌握 Publisher/Operator/Subscriber、常用操作符、线程调度、错误处理四块即可应对绝大多数场景;与 SwiftUI 搭配时,用 @Published + assign 让数据流自动驱动 UI。新项目可优先 Combine,老项目可逐步把回调替换为 Publisher。
- 点赞
- 收藏
- 关注作者
评论(0)