iOS Combine 响应式框架

举报
yd_217846120 发表于 2026/09/27 11:37:44 2026/09/27
【摘要】 iOS Combine 响应式框架摘要:Combine 是 Apple 官方的响应式编程框架,提供 Publisher/Operator/Subscriber 三件套,统一处理异步事件流。本文讲解核心概念、常用操作符、与 SwiftUI 集成及实战模式。 一、Combine 解决什么问题iOS 开发中异步来源繁多:网络回调、KVO、通知中心、按钮事件、定时器。每种 API 形态各异,组合起...

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。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。