Combine 入门与实战
Combine 入门与实战
Combine 框架提供了一套强大且一致的 API,用于处理异步事件和数据流。
本文介绍 Combine 的基本概念、优势,并通过系统 Publisher 和自定义 Publisher 示例,展示如何在实际项目中应用 Combine。 同时还解释了一些关键函数的使用方法。
什么是 Combine?
Combine 是 Apple 在 WWDC 2019 上发布的一个响应式编程框架,旨在简化和统一异步编程。它使用声明式编程模型,通过 Publisher 和 Subscriber 来处理异步事件。
在了解 Combine 框架之前,需要了解一些基本概念:
- Publisher(发布者): 发布者是数据流的源头,它会在未来某个时间点发布值或完成事件(包括失败事件)。常见的发布者有
Just
、Future
、PassthroughSubject
和CurrentValueSubject
。 - Subscriber(订阅者): 订阅者是数据流的终点,它会订阅发布者并对发布者发布的值或事件作出响应。常见的订阅者有
Sink
和Assign
。 - Operator(操作符): 操作符用于对发布者发布的数据进行变换或处理,比如
map
、filter
、flatMap
等。 - Cancellable(取消订阅): 当订阅者不再需要数据流时,可以取消订阅,释放资源。
使用 Combine 的优势
- 一致性和可组合性:Combine 提供了一套统一的 API,可以处理不同类型的异步事件和数据流,如网络请求、计时器、用户输入等。
- 简化错误处理:Combine 内建了错误处理机制,可以轻松捕获和处理异步操作中的错误。
- 简化内存管理:Combine 的 Cancellable 机制有助于更好地管理内存,避免内存泄漏。
- 更好的测试性:Combine 的声明式编程模型使得代码更容易测试,可以方便地模拟各种异步事件和数据流。
系统 Publisher 示例
以下是一个使用系统提供的 Publisher 示例,展示如何使用 Combine 处理网络请求并更新 UI。
定义网络服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Combine
import Foundation
struct User: Codable, Identifiable {
let id: Int
let name: String
}
class UserService {
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [User].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
定义视图模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import SwiftUI
import Combine
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
private let userService = UserService()
func startFetchingUsers() {
userService.fetchUsers()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
self.errorMessage = error.localizedDescription
}
}, receiveValue: { users in
self.users = users
})
.store(in: &cancellables)
}
}
SwiftUI 视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct ContentView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
VStack {
List(viewModel.users) { user in
Text(user.name)
}
if let errorMessage = viewModel.errorMessage {
Text("Error: \(errorMessage)")
.foregroundColor(.red)
.padding()
}
Button("Start Fetching Users") {
viewModel.startFetchingUsers()
}
.padding()
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
自定义 Publisher 示例
系统提供的 Publisher 足以处理许多常见的异步任务,但你可能需要创建自定义的 Publisher 来处理特定的需求。
为了在 Combine 中创建自定义的 Publisher,你需要遵循 Publisher 协议并实现其必需的方法:receive(subscriber:)
和 Output、Failure 类型别名。自定义的 Publisher 可以用于发布自定义事件或数据流,例如从自定义数据源获取数据或处理特定事件。
下面是一个示例,展示如何创建一个自定义的 Publisher,并在你的视图模型中使用它。
定义自定义 Publisher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import Combine
import Foundation
struct TimerPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
private let interval: TimeInterval
init(interval: TimeInterval) {
self.interval = interval
}
func receive<S>(subscriber: S) where S : Subscriber, TimerPublisher.Failure == S.Failure, TimerPublisher.Output == S.Input {
let subscription = TimerSubscription(subscriber: subscriber, interval: interval)
subscriber.receive(subscription: subscription)
}
}
final class TimerSubscription<S: Subscriber>: Subscription where S.Input == Int, S.Failure == Never {
private var subscriber: S?
private var timer: Timer?
private var counter = 0
init(subscriber: S, interval: TimeInterval) {
self.subscriber = subscriber
self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
self?.tick()
}
}
func request(_ demand: Subscribers.Demand) {
// 处理背压(Backpressure)的请求
}
func cancel() {
timer?.invalidate()
timer = nil
subscriber = nil
}
private func tick() {
_ = subscriber?.receive(counter)
counter += 1
}
}
使用自定义 Publisher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import SwiftUI
import Combine
class TimerViewModel: ObservableObject {
@Published var counter: Int = 0
private var cancellables = Set<AnyCancellable>()
func startTimer() {
TimerPublisher(interval: 1.0)
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.counter = value
}
.store(in: &cancellables)
}
func stopTimer() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}
}
struct ContentView: View {
@StateObject private var viewModel = TimerViewModel()
var body: some View {
VStack {
Text("Counter: \(viewModel.counter)")
.font(.largeTitle)
.padding()
Button("Start Timer") {
viewModel.startTimer()
}
.padding()
Button("Stop Timer") {
viewModel.stopTimer()
}
.padding()
}
}
}
说明:
- 定义自定义 Publisher:
- TimerPublisher 遵循 Publisher 协议,并实现了 receive(subscriber:) 方法。
- TimerSubscription 是一个 Subscription,它负责创建一个计时器并向订阅者发布值。
- 使用自定义 Publisher:
- 在视图模型 TimerViewModel 中,使用 TimerPublisher 创建一个每秒发布一次计时器事件的发布者。
- 通过 sink(receiveValue:) 订阅这个发布者,并将接收到的值更新到 counter 状态。
在基于 UIKit App 中的使用示例
由于 SwiftUI 本身集成了 Combine,很多实现基于 @Stete @Binding @StateObject 这些就很容易实现。 下面是一个 UIKit 中应用, 比如:页面中监听 UITextField 文本变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
import Combine
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var tf: UITextField!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object:tf)
.map( { ($0.object as! UITextField).text ?? ""} )
// sink 创建订阅者,获取完成值和每次订阅值
//.sink(receiveCompletion: { print($0) },
// receiveValue: {
// self.label.text = $0
//})
// assign 创建订阅者,用于订阅值直接赋值,需要用键值操作赋值
//.assign(to: \.self.label!.text, on: self)
.assign(to: \UILabel.text, on: self.label)
.store(in: &cancellables)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
}
关键函数解释
1. sink
sink
是一个订阅操作符,会生成订阅者,用于接收发布者发布的数据和完成事件。它有两种常见的用法:
接收值:这个变体只处理发布者发布的值,不处理完成事件。
1
func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
接收完成事件和值:这个变体可以同时处理发布者的完成事件和发布的值。
1
func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
2. assign
assign
是一个订阅操作符,会生成订阅者,它用于拿到目标结果直接赋值的情况。需要用 key path 指定赋值对象
1
2
3
4
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object:tf)
.map( { ($0.object as! UITextField).text ?? ""} )
.assign(to: \UILabel.text, on: self.label)
3. store(in:)
store(in:)
用于将 AnyCancellable
对象存储在集合中,以便统一管理和取消订阅。通常在视图模型中使用,以便在视图模型销毁时自动取消订阅,避免内存泄漏。
1
2
3
4
5
6
7
private var cancellables = Set<AnyCancellable>()
publisher
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
4. eraseToAnyPublisher
eraseToAnyPublisher
用于将具体的发布者类型转换为 AnyPublisher
,从而隐藏发布者的具体类型。这在需要返回通用发布者时非常有用。
1
2
3
4
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [User].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
5. flatMap
flatMap
用于将一个发布者的输出转换为另一个发布者,并将其扁平化,以便在链式操作中处理嵌套的发布者。
1
2
3
4
5
6
7
8
9
10
11
Timer.publish(every: 10.0, on: .main, in: .common)
.autoconnect()
.flatMap { _ in
self.userService.fetchUsers()
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink(receiveValue: { users in
self.users = users
})
.store(in: &cancellables)
6. catch
catch
用于捕获发布者发出的错误,并提供一个替代的发布者,以便在错误发生时继续发布数据流。
1
2
3
4
5
6
7
8
userService.fetchUsers()
.catch { error in
Just([]) // 返回一个空数组作为默认值
}
.sink(receiveValue: { users in
self.users = users
})
.store(in: &cancellables)
总结
Combine 框架为 Swift 开发者提供了一套强大且统一的工具,用于处理异步事件和数据流。通过理解基本概念和关键函数,你可以更高效地编写响应式代码,并在项目中利用 Combine 的优势。无论是系统提供的 Publisher 还是自定义的 Publisher,Combine 都能帮助你简化异步编程,提升代码的可读性和可维护性。
希望这篇文章能帮助你更好地理解和应用 Combine 框架。
参考
https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine