Apple处理异步任务的Combine框架
Apple处理异步任务的Combine框架

Apple处理异步任务的Combine框架

Combine 是 Apple 推出的一个框架,用于处理异步事件和响应式编程。它可以帮助开发者以声明性的方式处理复杂的异步任务,例如网络请求、定时器、UI 更新等,并且能避免回调地狱(Callback Hell)。

在 SwiftUI 中,Combine 被大量用来处理数据绑定、网络请求、UI 状态管理等。

核心概念

1、Publisher(发布者)

提供数据流,可以是单个值或多个值。代表事件的来源,比如网络请求结果、用户输入、定时器触发等。

常见的内置 Publisher

1)Just:发布一个单一值。

Just 是一种特殊的 Publisher,它只发布一个单一的值,然后立即完成。适用于需要立即返回一个简单值的场景。

特点:只能发布一个值,一旦创建,就不可修改,发布后立即完成订阅。

import Combine

let justPublisher = Just(42)

let subscription = justPublisher.sink(
    receiveCompletion: { completion in
        print("Completion: \(completion)") // 打印 "Completion: finished"
    },
    receiveValue: { value in
        print("Value: \(value)") // 打印 "Value: 42"
    }
)

2)PassthroughSubject:动态发布多个值。

它不会保留任何先前发送的值,新的订阅者只会收到订阅之后的值。常用于处理用户交互、通知或其他需要即时传播的事件。

特点:没有初始值,只会发布订阅后产生的值,手动调用 .send(_:) 发布数据。

import Combine

let subject = PassthroughSubject<String, Never>()

let subscription = subject.sink(
    receiveCompletion: { completion in
        print("Completion: \(completion)")
    },
    receiveValue: { value in
        print("Value: \(value)")
    }
)

subject.send("Hello") // 打印 "Value: Hello"
subject.send("World") // 打印 "Value: World"
subject.send(completion: .finished) // 打印 "Completion: finished"

3)CurrentValueSubject:持有一个当前值,并可以发布新的值。

每次订阅时,订阅者都会收到当前值,然后再接收之后的值。常用于状态管理或需要持有某个值的场景。

特点:持有一个初始值,任何订阅者都会先收到当前值,手动调用 .send(_:) 更新值。

import Combine

let currentValueSubject = CurrentValueSubject<Int, Never>(10)

let subscription1 = currentValueSubject.sink(
    receiveCompletion: { completion in
        print("Completion: \(completion)")
    },
    receiveValue: { value in
        print("Subscription1 Value: \(value)")
    }
)

currentValueSubject.send(20) // Subscription1 收到 20

let subscription2 = currentValueSubject.sink(
    receiveValue: { value in
        print("Subscription2 Value: \(value)")
    }
)

// Subscription2 会先收到当前值 20
currentValueSubject.send(30) // 两个订阅者都收到 30

// 输出:
// Subscription1 Value: 10
// Subscription1 Value: 20
// Subscription2 Value: 20
// Subscription1 Value: 30
// Subscription2 Value: 30

4)Swift标准库的Combine扩展

除了上述的三个内置Publisher外,Swift 标准库中还有多个类型被扩展为 Combine 的 Publisher。这些扩展很多是由 Apple 直接在 Combine 框架中提供的,有些是通过 Foundation 框架间接支持的。

常见被扩展为Publisher的类型列表:

1、Array, Set, Dictionary, Range:.publisher方法,变成 Publishers.Sequence。

2、Optional:publisher方法,有值时发出1个值;nil时直接 .finished。

3、NotificationCenter:.publisher(for:)方法,监听系统通知,变成 Publisher。

4、URLSession:.dataTaskPublisher(for:)方法,网络请求直接返回 Publisher。

5、KVO 对象:.publisher(for:)方法,键值观察(KVO)也可以变成 Publisher。

6、Timer:Timer.publish(…)方法,定时器事件流,支持 .autoconnect()。

7、Just / Future / PassthroughSubject / CurrentValueSubject,Combine 原生构造器,已知的 Publisher 类型构建器。

2、Subscriber(订阅者)

订阅 Publisher,接收其发布的数据。

开发者可以实现自己的订阅者,或者使用 Combine 提供的内置方法,例如 .sink 或 .assign。

import Combine

let publisher = Just("Hello, Combine!")

let subscriber = publisher.sink { value in
    print("Received value: \(value)")
}
// 输出:Received value: Hello, Combine!

1)sink:它是 Combine 中最常用、最直观的订阅方法之一。它本质上就是一个“订阅者(Subscriber)构造器”。

1、完整的 .sink 示例

let cancellable = somePublisher.sink(
    receiveCompletion: { completion in
        // 订阅完成(正常完成或失败)时调用
    },
    receiveValue: { value in
        // 每次收到 Publisher 发出的值时调用
    }
)

somePublisher 是一个 Combine Publisher,比如 Just(5) 或 PassthroughSubject<Int, Never>()。

.sink() 返回一个 AnyCancellable,用于管理生命周期(取消订阅)。

2、.sink 和订阅者(Subscriber)之间的关系

在 Combine 中:Publisher 只有在被订阅(Subscribe)之后才开始“发出事件”。

.sink 本质上是创建了一个匿名的订阅者(Subscriber),用于接收值和完成事件。

当 .sink 创建了一个订阅者,隐式实现了 Subscriber 协议。在建立连接时,Publisher 发送数据。.sink 接收 value 和 completion,调用传入的闭包。.sink返回返回 AnyCancellable时,可用于取消订阅(释放内存)。

可以理解为:

let subscriber = Subscriber(...) // 隐式构建
publisher.subscribe(subscriber)  // 等价于 .sink(...)

3、.sink有两种常用写法:

1)简单写法

sink { value in ... }

2)完整写法

sink(
    receiveCompletion: { completion in ... },
    receiveValue: { value in ... }
)

这两种写法都合法,第二种是 sink(receiveValue:) 的简写形式,在不关心 .finished 或 .failure 时使用。

.sink(receiveValue:),用于简介监听值,不处理结束或错误。

.sink(receiveCompletion:, receiveValue:),功能更完整,接收 .finished 或 .failure。

4、.sink返回的是什么?

let cancellable: AnyCancellable = ...

这是一个 Combine 提供的类型,用于取消订阅(手动释放资源):

cancellable.cancel()

如果把 cancellable 丢了,Publisher 会照常发,但没人监听它。

5、和SwiftUI的关系

在 SwiftUI 中,推荐使用 .onReceive() 代替 .sink(),因为:

.onReceive():自动管理生命周期(绑定视图)。

.sink():需要手动保留 AnyCancellable,否则被释放了就不再接收事件。

3、Operator(操作符)

用于对数据流进行变换、过滤、合并等操作,类似于 Swift 的 map、filter、reduce。

常用操作符

map:将一个值映射为另一个值。

filter:筛选满足条件的数据。

combineLatest:合并多个 Publisher 的最新值。

flatMap:将一个 Publisher 的值映射为另一个 Publisher。

import Combine

let publisher = Just(5)

let mappedPublisher = publisher.map { $0 * 2 } // 乘以 2

mappedPublisher.sink { value in
    print("Mapped value: \(value)") // 输出:Mapped value: 10
}	

4、Cancellable(取消任务)

订阅后返回的对象,可以通过调用 .cancel() 停止订阅,避免内存泄漏。

5、Subjects(主题)

既是 Publisher,也是 Subscriber。

可用于手动触发值的发布。

PassthroughSubject:初始没有值,仅发送接收到的值。

CurrentValueSubject:保存当前值,并向新订阅者发送最新值。

使用场景

1、数据流处理

import Combine

// 1. 创建一个 Publisher
let numbers = [1, 2, 3, 4, 5].publisher

// 2. 使用 Operator 转换数据
let subscription = numbers
    .filter { $0 % 2 == 0 }  // 只保留偶数
    .map { $0 * 10 }         // 将每个值乘以 10
    .sink { value in
        print("Received value: \(value)")
    }

// 输出:
// Received value: 20
// Received value: 40

2、Optional 变成 Publisher:

let someValue: Int? = 10
someValue.publisher
    .sink { print("Value: \($0)") }
// 输出:Value: 10

let noneValue: Int? = nil
noneValue.publisher
    .sink { print("不会有输出") }
// 输出:无(直接完成)

3、Timer发布时间流:

let timer = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .sink { time in
        print("Tick: \(time)")
    }

4、网络请求 + SwiftUI

import SwiftUI
import Combine

class APIViewModel: ObservableObject {
    @Published var data: String = "Loading..."
    private var cancellable: AnyCancellable?

    func fetchData() {
        let url = URL(string: "https://api.example.com/data")!
        cancellable = URLSession.shared.dataTaskPublisher(for: url)
            .map { String(data: $0.data, encoding: .utf8) ?? "No Data" }
            .replaceError(with: "Error fetching data")
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in self?.data = $0 }
    }
}

struct APIView: View {
    @StateObject private var viewModel = APIViewModel()

    var body: some View {
        VStack {
            Text(viewModel.data)
                .padding()
            Button("Fetch Data") {
                viewModel.fetchData()
            }
        }
        .onAppear {
            viewModel.fetchData()
        }
    }
}

与 SwiftUI 集成:@Published 和 @StateObject

在 MVVM 架构中,Combine 常用于连接 ViewModel 和 View。SwiftUI 提供了便捷的 @Published 和 @StateObject,简化了响应式编程。

示例:ViewModel 与 View 的绑定

import SwiftUI
import Combine

// ViewModel
class CounterViewModel: ObservableObject {
    @Published var count: Int = 0

    func increment() {
        count += 1
    }
}

// View
struct CounterView: View {
    @StateObject private var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
                .font(.largeTitle)
            Button("Increment") {
                viewModel.increment()
            }
            .padding()
        }
    }
}

1、Swift代码:ObservableObject和@Published都来自Combine,底层全部依赖Combine的Publisher机制来驱动UI刷新。

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0
}

ObservableObject负责协议,提供对象变更通知。@Published是属性包装器,会自动生成一个 Publisher:$count。

2、SwiftUI视图:@StateObject是SwiftUI的属性包装器:

@StateObject private var viewModel = CounterViewModel()

但它要求传入的对象必须是 ObservableObject,也就是说@StateObject 是 SwiftUI 的入口,但真正监听数据变化、触发视图刷新,是由 Combine 驱动的。

3、Combine:如果不导入Combine,实际上也可以编译:

import SwiftUI
// import Combine  不导入Combine

这是因为,SwiftUI默认已经隐式地依赖了Combine。

@Published、ObservableObject 都来自 Combine,但是只要用 SwiftUI,编译器能自动找到这些引用,所以看起来“没导入也能用”。

优点

1、声明式编程:代码可读性高,逻辑清晰。

2、易于组合:通过操作符轻松处理数据流的复杂逻辑。

3、线程管理:通过 .receive(on:) 控制数据在哪个线程中处理。

总结

从 Swift 5.9(iOS 17)开始,@Observable 取代 ObservableObject,但它目前仍处于实验阶段,并且有以下区别:

在学习Combine框架时,如果比较困难,同时App只支持iOS 17+,那么可以考虑使用@Observable替代@ ObservableObject。

相关文章

1、Swift @Observable属性包装器:https://fangjunyu.com/2024/11/03/swift-observable%e5%b1%9e%e6%80%a7%e5%8c%85%e8%a3%85%e5%99%a8/

2、SwiftUI状态管理机制@Observable和@Environment:https://fangjunyu.com/2024/12/23/swiftui%e7%8a%b6%e6%80%81%e7%ae%a1%e7%90%86%e6%9c%ba%e5%88%b6observable%e5%92%8cenvironment/

3、Swift常见的支持 Publisher 响应式处理的类或场景:https://fangjunyu.com/2024/12/14/swift%e5%b8%b8%e8%a7%81%e7%9a%84%e6%94%af%e6%8c%81-publisher-%e5%93%8d%e5%ba%94%e5%bc%8f%e5%a4%84%e7%90%86%e7%9a%84%e7%b1%bb%e6%88%96%e5%9c%ba%e6%99%af/

4、Swift和Foundation框架创建和管理定时任务的Timer类:https://fangjunyu.com/2024/12/14/swift%e5%92%8cfoundation%e6%a1%86%e6%9e%b6%e5%88%9b%e5%bb%ba%e5%92%8c%e7%ae%a1%e7%90%86%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1%e7%9a%84timer%e7%b1%bb/

5、SwiftUI使用Combine监听并动态管理菜单栏案例:https://fangjunyu.com/2025/07/11/swiftui%e4%bd%bf%e7%94%a8combine%e7%9b%91%e5%90%ac%e5%b9%b6%e5%8a%a8%e6%80%81%e7%ae%a1%e7%90%86%e8%8f%9c%e5%8d%95%e6%a0%8f%e6%a1%88%e4%be%8b/

扩展知识

内置Publisher类型

在文中主要讲了三个内置构造器Just、PassthroughSubject、CurrentValueSubject,还有标准库扩展:.publisher on Array、Optional、NotificationCenter、URLSession 等。

实际上还有更多内置Publisher类型,但它们大多数是“组合或封装型”的,用于更高级场景。

常见Publisher类型总览

1、Just:发送一个值后结束,用于测试、默认值。

2、Fail:立即失败,用于模拟错误流。

3、Empty:不发送任何值,用于占位流、条件跳过。

4、Deferred:延迟创建 Publisher,按需惰性生成。

5、Future:执行一次异步任务后发出值,用于网络请求、延迟操作。

6、Record:发送固定事件序列,用于测试使用。

7、Publishers.Sequence:来自 .publisher 的数组等,用于批量静态值。

8、Publishers.Once:.publisher from Optional,一次性 optional。

9、Publishers.Share:多个订阅共享同一输出,网络共享。

10、Publishers.Timer:来自 Timer.publish(),用于定时事件流。

11、Publishers.Merge, Zip, CombineLatest:组合多个 Publisher,响应多个数据源。

12、Publishers.HandleEvents, TryMap, 等操作符变种:添加副作用、错误捕获等,用于调试、流程控制。

最小可用Publisher列表

1、Just:单值快速测试,建议掌握。

2、PassthroughSubject:通知型手动流,建议掌握。

3、CurrentValueSubject:状态型手动流,建议掌握。

4、Future:异步任务封装,建议掌握。

5、Empty, Fail:用于控制流结构,可选了解。

6、Deferred:惰性、按需场景,可选了解。

autoconnect()定时器

autoconnect() 是 Combine 中用于定时器(Timer.publish(…))的快捷方法,它的作用是:自动连接定时器的 Publisher,使其立即开始发出事件,而不需要手动调用 .connect()。

当使用Timer.publish创建ConnectablePublisher时:

let timer = Timer.publish(every: 1.0, on: .main, in: .common)

它返回的不是一个普通的Publisher,而是一个:

Publishers.Autoconnect<Publishers.MakeConnectable<Timer.TimerPublisher>>

也就是说,它是“可连接的 Publisher”——默认情况下不会自动开始发出事件,除非手动调用 .connect()。

如果不使用autoconnect() 时:必须手动 .connect()。

let publisher = Timer.publish(every: 1.0, on: .main, in: .common)

let subscription = publisher
    .sink { date in
        print("Tick: \(date)")
    }

let connection = publisher.connect() // 必须手动启动它

使用 .autoconnect() 简化写法

let subscription = Timer
    .publish(every: 1.0, on: .main, in: .common)
    .autoconnect() // 自动启动定时器
    .sink { date in
        print("Tick: \(date)")
    }

.receive(on:) 有哪些选项?

这个方法的作用是指定在哪个线程/调度器(Scheduler)上接收事件。常见的几个:

1、RunLoop.main:在主线程(UI 线程)接收事件。UI 更新必须使用这个,否则会崩溃或无效。适用于 macOS/iOS App。

.receive(on: RunLoop.main)

2、DispatchQueue.main

也在主线程,但使用 GCD 的方式调度。和 RunLoop.main 类似,差别非常小,主要取决于用的架构。

.receive(on: DispatchQueue.main)

3、DispatchQueue.global(qos: .background)(或 .utility, .userInitiated 等)

切换到后台线程进行耗时操作(例如下载、计算、文件读写等)。

.receive(on: DispatchQueue.global(qos: .background))

4、OperationQueue

更加细粒度的调度器控制,适用于复杂任务依赖或并发控制。

.receive(on: OperationQueue.main)

5、.subscribe(on:) vs .receive(on:)(额外补充)

.subscribe(on:):指定订阅者的工作(如网络请求、文件读取)在哪个线程执行。

.receive(on:):指定收到数据后回调在哪个线程执行(如 UI 更新)。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注