Combine 框架,從0到1 —— 5.Combine 提供的釋出者(Publishers)
阿新 • • 發佈:2020-09-25
本文首發於 [Ficow Shen's Blog](https://ficowshen.com),原文地址: [Combine 框架,從0到1 —— 5.Combine 提供的釋出者(Publishers)](https://blog.ficowshen.com/page/post/21)。
## 內容概覽
- 前言
- Just
- Future
- Deferred
- Empty
- Publishers.Sequence
- Fail
- Record
- Share
- Multicast
- ObservableObject
- @Published
- 總結
## 前言
正所謂,工欲善其事,必先利其器。在開始使用 `Combine` 進行響應式程式設計之前,建議您先了解 `Combine` 為您提供的各種釋出者(Publishers)、操作符(Operators)、訂閱者(Subscribers)。合理地選擇符合需求的 `Combine` 釋出者,可以大幅度地提升您的開發效率!
這些都是 `Combine` 為我們提供的釋出者:
`Just`,`Future`,`Deferred`,`Empty`,`Publishers.Sequence`,`Fail`,`Record`,`Share`,`Multicast`,`ObservableObject`,`@Published`。
接下來的幾分鐘,讓我們把它們各個擊破!
請注意,後續內容中出現的 `cancellables` 全部由這個類的例項提供 :
``` swift
final class CombinePublishersDemo {
private var cancellables = Set()
}
```
示例程式碼 Github 倉庫:[CombinePublishersDemo](https://github.com/FicowShen/Ficow-Combine-SwiftUI/blob/master/CombineDemo/CombineDemo/CombinePublishersDemo.swift)
## Just
[官網文件](https://developer.apple.com/documentation/combine/just)
`Just` 向每個訂閱者只發送單個值,然後結束。它的失敗型別為 `Never`,也就是不能失敗。 示例程式碼:
``` swift
func just() {
Just(1) // 直接傳送1
.sink { value in
// 輸出:just() 1
print(#function, value)
}
.store(in: &cancellables)
}
```
輸出內容:
> just() 1
`Just` 常被用在錯誤處理中,在捕獲異常後傳送備用值。 示例程式碼:
``` swift
func just2() {
// 使用 Fail 傳送失敗
Fail(error: NSError(domain: "", code: 0, userInfo: nil))
.catch { _ in
// 捕獲錯誤,返回 Just(3)
return Just(3)
}
.sink { value in
// 輸出:just2() 3
print(#function, value)
}
.store(in: &cancellables)
}
```
輸出內容:
> just2() 3
## Future
[官網文件](https://developer.apple.com/documentation/combine/future)
`Future` 使用一個閉包來進行初始化,最終這個閉包將執行傳入的一個閉包引數(promise)來發送**單個值**或者**失敗**。請不要使用 `Future` 傳送多個值。`PassthroughSubject`, `CurrentValueSubject` 或者 `Deferred` 會是更好的選擇。
請注意,`Future` 不會等待訂閱者傳送需求,它會**在被建立時就立刻非同步執行**這個初始化時傳入的閉包!如果你需要等待訂閱者傳送需求時才執行這個閉包,請使用 `Deferred`。如果你需要重複執行這個閉包,也請使用 `Deferred`。
示例程式碼:
``` swift
func future() {
Future { promise in
// 延時1秒
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success(2))
}
}
.sink { value in
// 輸出:future() 2
print(#function, value)
}
.store(in: &cancellables)
}
```
輸出內容:
> future() 2
更常見的用法是將 `Future` 作為一個任務函式的返回值,讓具體任務的執行程式碼與訂閱程式碼分離:
``` swift
private func bigTask() -> Future {
return Future() { promise in
// 模擬耗時操作
sleep(1)
guard Bool.random() else {
promise(.failure(NSError(domain: "com.ficowshen.blog", code: -1, userInfo: [NSLocalizedDescriptionKey: "task failed"])))
return
}
promise(.success(3))
}
}
func future2() {
bigTask()
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
// 輸出:future2() finished
print(#function, "finished")
case .failure(let error):
// 輸出:future2() Error Domain=com.ficowshen.blog Code=-1 "task failed" UserInfo={NSLocalizedDescription=task failed}
print(#function, error)
}
}, receiveValue: { value in
// 輸出:future2() 3
print(#function, value)
})
.store(in: &cancellables)
}
```
輸出內容由 `Bool.random()` 決定,可能是:
> future2() Error Domain=com.ficowshen.blog Code=-1 "task failed" UserInfo={NSLocalizedDescription=task failed}
也可能是:
> future2() 3
> future2() finished
## Deferred
[官網文件](https://developer.apple.com/documentation/combine/deferred)
`Deferred` 使用一個生成釋出者的閉包來完成初始化,這個閉包會在訂閱者執行訂閱操作時才執行。
示例程式碼:
``` swift
func deferred() {
let deferredPublisher = Deferred> {
// 在訂閱之後才會執行
print(Date(), "Future inside Deferred created")
return Future { promise in
promise(.success(true))
}.eraseToAnyPublisher()
}.eraseToAnyPublisher()
print(Date(), "Deferred created")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// 延遲1秒後進行訂閱
deferredPublisher
.sink(receiveCompletion: { completion in
print(Date(), "Deferred receiveCompletion:", completion)
}, receiveValue: { value in
print(Date(), "Deferred receiveValue:", value)
})
.store(in: &self.cancellables)
}
}
```
執行上面的函式,輸出內容如下(請注意觀察輸出的時間,延時1秒):
> 2020-09-08 23:44:35 +0000 Deferred created
> 2020-09-08 23:44:36 +0000 Future inside Deferred created
> 2020-09-08 23:44:36 +0000 Deferred receiveValue: true
> 2020-09-08 23:44:36 +0000 Deferred receiveCompletion: finished
## Empty
[官網文件](https://developer.apple.com/documentation/combine/empty)
`Empty` 是一個從不釋出任何值的釋出者,可以選擇立即完成(`Empty()` 或者 `Empty(completeImmediately: true)`)。
> 可以使用 `Empty(completeImmediately: false)` 建立一個**從不**釋出者(一個從不傳送值,也從不完成或失敗的釋出者)。
`Empty` 常用於錯誤處理。當錯誤發生時,如果你不想傳送錯誤,可以用 `Empty` 來發送完成。
示例程式碼:
``` swift
func empty() {
Empty() // 或者 Empty(completeImmediately: true)
.sink(receiveCompletion: { completion in
// 輸出:empty() finished
print(#function, completion)
}, receiveValue: { _ in
})
.store(in: &self.cancellables)
}
```
輸出內容:
> empty() finished
## Publishers.Sequence
[官網文件](https://developer.apple.com/documentation/combine/publishers/sequence)
`Publishers.Sequence` 是傳送一個元素序列的釋出者,元素髮送完畢時會自動傳送完成。
``` swift
func sequence() {
[1, 2, 3].publisher
.sink(receiveCompletion: { completion in
// 輸出:sequence() finished
print(#function, completion)
}, receiveValue: { value in
// 輸出:sequence() 1
// 輸出:sequence() 2
// 輸出:sequence() 3
print(#function, value)
})
.store(in: &self.cancellables)
}
```
輸出內容:
> sequence() 1
> sequence() 2
> sequence() 3
> sequence() finished
## Fail
[官網文件](https://developer.apple.com/documentation/combine/fail)
`Fail` 是一個以指定的錯誤終止序列的釋出者。通常用於返回錯誤,比如:在校驗引數缺失或錯誤等場景中,返回一個 `Fail`。
示例程式碼:
``` swift
func fail() {
Fail(error: NSError(domain: "", code: 0, userInfo: nil))
.sink(receiveCompletion: { completion in
// 輸出:fail() failure(Error Domain= Code=0 "(null)")
print(#function, completion)
}, receiveValue: { _ in
})
.store(in: &cancellables)
}
```
## Record
[官網文件](https://developer.apple.com/documentation/combine/record)
`Record` 釋出者允許錄製一系列的輸入和一個完成,錄製之後再發送給每一個訂閱者。
示例程式碼:
``` swift
func record() {
Record { record in
record.receive(1)
record.receive(2)
record.receive(3)
record.receive(completion: .finished)
}
.sink(receiveCompletion: { completion in
// 輸出:record() finished
print(#function, completion)
}, receiveValue: { value in
// 輸出:record() 1
// 輸出:record() 2
// 輸出:record() 3
print(#function, value)
})
.store(in: &cancellables)
}
```
輸出內容:
> record() 1
> record() 2
> record() 3
> record() finished
## Share
[官網文件](https://developer.apple.com/documentation/combine/publishers/share)
`Share` 釋出者可以和多個訂閱者共享上游釋出者的輸出。請注意,它和其他`值型別`的釋出者不一樣,這是一個`引用型別`的釋出者!
當您需要使用引用語義的釋出者時,可以考慮使用這個型別。
為了更好地理解 `Share` 的意義和用途, 讓我們先來觀察沒有 `Share` 會出現什麼問題:
``` swift
func withoutShare() {
let deferred = Deferred> {
print("creating Future")
return Future { promise in
print("promise(.success(1))")
promise(.success(1))
}
}
deferred
.print("1_")
.sink(receiveCompletion: { completion in
print("receiveCompletion1", completion)
}, receiveValue: { value in
print("receiveValue1", value)
})
.store(in: &cancellables)
deferred
.print("2_")
.sink(receiveCompletion: { completion in
print("receiveCompletion2", completion)
}, receiveValue: { value in
print("receiveValue2", value)
})
.store(in: &cancellables)
}
```
輸出內容:
> creating Future
promise(.success(1))
1_: receive subscription: (Future)
1_: request unlimited
1_: receive value: (1)
receiveValue1 1
1_: receive finished
receiveCompletion1 finished
creating Future
promise(.success(1))
2_: receive subscription: (Future)
2_: request unlimited
2_: receive value: (1)
receiveValue2 1
2_: receive finished
receiveCompletion2 finished
通過觀察輸出內容,我們可以發現 Deferred 和 Future 部分的程式碼`執行了兩次`!
接下來,我們使用 `Share` 來嘗試解決這個問題:
``` swift
func withShare() {
let deferred = Deferred> {
print("creating Future")
return Future { promise in
print("promise(.success(1))")
promise(.success(1))
}
}
let sharedPublisher = deferred
.print("0_")
.share()
sharedPublisher
.print("1_")
.sink(receiveCompletion: { completion in
print("receiveCompletion1", completion)
}, receiveValue: { value in
print("receiveValue1", value)
})
.store(in: &cancellables)
sharedPublisher
.print("2_")
.sink(receiveCompletion: { completion in
print("receiveCompletion2", completion)
}, receiveValue: { value in
print("receiveValue2", value)
})
.store(in: &cancellables)
}
```
輸出內容:
> 1_: receive subscription: (Multicast)
1_: request unlimited
creating Future
promise(.success(1))
0_: receive subscription: (Future)
0_: request unlimited
0_: receive value: (1)
1_: receive value: (1)
receiveValue1 1
0_: receive finished
1_: receive finished
receiveCompletion1 finished
2_: receive subscription: (Multicast)
2_: request unlimited
2_: receive finished
receiveCompletion2 finished
咦,Deferred 和 Future 部分執行了兩次的問題解決了,但是`出現了另一個問題`!第二個訂閱者沒有收到值,只收到了完成!!??
而且,仔細觀察輸出的內容,`Multicast` 十分引人注目!
原來,根據官方文件的解釋,`Share` 其實是 `Multicast` 釋出者和 `PassthroughSubject` 釋出者的結合,而且它會隱式呼叫 `autoconnect()`。
也就是說,在訂閱操作發生後,`Share` 就會開始傳送內容。這樣也就導致了後續的訂閱者無法收到之前就已經發布的值。
怎麼解決這個問題?
回顧 [Combine 框架,從0到1 —— 2.通過 ConnectablePublisher 控制何時釋出](https://blog.ficowshen.com/page/post/13) 的內容,我們可以通過自行呼叫 `connect()` 來解決這個問題。
這是調整後的程式碼:
``` swift
func withShareAndConnectable() {
let deferred = Deferred> {
print("creating Future")
return Future { promise in
print("promise(.success(1))")
promise(.success(1))
}
}
let sharedPublisher = deferred
.print("0_")
.share()
.makeConnectable() // 自行決定釋出者何時開始傳送訂閱元素給訂閱者
sharedPublisher
.print("1_")
.sink(receiveCompletion: { completion in
print("receiveCompletion1", completion)
}, receiveValue: { value in
print("receiveValue1", value)
})
.store(in: &cancellables)
sharedPublisher
.print("2_")
.sink(receiveCompletion: { completion in
print("receiveCompletion2", completion)
}, receiveValue: { value in
print("receiveValue2", value)
})
.store(in: &cancellables)
sharedPublisher
.connect() // 讓釋出者開始傳送內容
.store(in: &cancellables)
}
```
只需要在 `share()` 之後呼叫 `makeConnectable()`,我們即可奪回控制權!在所有訂閱者準備就緒之後,通過呼叫 `connect()` 讓釋出者開始傳送內容。
輸出內容:
> 1_: receive subscription: (Multicast)
1_: request unlimited
2_: receive subscription: (Multicast)
2_: request unlimited
creating Future
promise(.success(1))
0_: receive subscription: (Future)
0_: request unlimited
0_: receive value: (1)
1_: receive value: (1)
receiveValue1 1
2_: receive value: (1)
receiveValue2 1
0_: receive finished
1_: receive finished
receiveCompletion1 finished
2_: receive finished
receiveCompletion2 finished
現在,Deferred 和 Future 部分的程式碼只執行一次,兩個訂閱者也都收到了值和完成。
除此之外,我們也可以使用 `Multicast` 解決這個問題。
## Multicast
[官網文件](https://developer.apple.com/documentation/combine/publishers/multicast)
`Multicast` 釋出者使用一個 `Subject` 向多個訂閱者傳送元素。和 `Share` 一樣,這也是一個引用型別的釋出者。在使用多個訂閱者進行訂閱時,它們可以有效地保證上游釋出者不重複執行繁重的耗時操作。
而且 `Multicast` 是一個 `ConnectablePublisher`,所以我們需要在訂閱者準備就緒之後去手動呼叫 `connect()` 方法,然後訂閱者才能收到上游釋出者傳送的元素。
示例程式碼:
``` swift
func multicast() {
let multicastSubject = PassthroughSubject()
let deferred = Deferred> {
print("creating Future")
return Future { promise in
print("promise(.success(1))")
promise(.success(1))
}
}
let sharedPublisher = deferred
.print("0_")
.multicast(subject: multicastSubject)
sharedPublisher
.print("1_")
.sink(receiveCompletion: { completion in
print("receiveCompletion1", completion)
}, receiveValue: { value in
print("receiveValue1", value)
})
.store(in: &cancellables)
sharedPublisher
.print("2_")
.sink(receiveCompletion: { completion in
print("receiveCompletion2", completion)
}, receiveValue: { value in
print("receiveValue2", value)
})
.store(in: &cancellables)
sharedPublisher
.connect()
.store(in: &cancellables)
}
```
輸出內容:
> 1_: receive subscription: (Multicast)
1_: request unlimited
2_: receive subscription: (Multicast)
2_: request unlimited
creating Future
promise(.success(1))
0_: receive subscription: (Future)
0_: request unlimited
0_: receive value: (1)
1_: receive value: (1)
receiveValue1 1
2_: receive value: (1)
receiveValue2 1
0_: receive finished
1_: receive finished
receiveCompletion1 finished
2_: receive finished
receiveCompletion2 finished
## ObservableObject
[官網文件](https://developer.apple.com/documentation/combine/observableobject)
`ObservableObject` 是具有釋出者的一種物件,該物件在更改物件之前發出變動元素。常用在 `SwiftUI` 中。
遵循 `ObservableObject` 協議的物件會自動生成一個 `objectWillChange` 釋出者,這個釋出者會在這個物件的 `@Published` 屬性發生變動時傳送 `變動之前的舊值`。
來自官網文件的示例程式碼:
``` swift
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
let john = Contact(name: "John Appleseed", age: 24)
john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
.store(in: &cancellables)
print(john.haveBirthday())
```
輸出內容:
> 24 will change
> 25
## @Published
[官網文件](https://developer.apple.com/documentation/combine/published)
`@Published` 是一個屬性包裝器([@propertyWrapper](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617)),它可以為任何屬性新增一個 `Combine` 釋出者。常用在 `SwiftUI` 中。
請注意,`@Published` 釋出的是屬性觀察器 `willSet` 中接收到的新值,但是這個屬性當前的值還是舊值!觀察下面的例子,可以幫助您理解這個重點。
來自官網文件的示例程式碼:
``` swift
class Weather {
@Published var temperature: Double
init(temperature: Double) {
self.temperature = temperature
}
}
func published() {
let weather = Weather(temperature: 20)
weather
.$temperature // 請注意這裡的 $ 符號,通過 $ 操作符來訪問釋出者
.sink() { value in
print("Temperature before: \(weather.temperature)") // 屬性中的值尚未改變
print("Temperature now: \(value)") // 釋出者釋出的是新值
}
.store(in: &cancellables)
weather.temperature = 25 // 請注意這裡沒有 $ 符號,訪問的是被屬性包裝器包裝起來的值
}
```
輸出內容:
> Temperature before: 20.0
> Temperature now: 20.0
> Temperature before: 20.0
> Temperature now: 25.0
當 `sink` 中收到新值 25.0 時,`weather.temperature` 的值依然為 20.0。
## 總結
感謝 `Combine` 為我們提供了這些釋出者:
Just,Future,Deferred,Empty,Publishers.Sequence,Fail,Record,Share,Multicast,ObservableObject,@Published
雖然看起來有很多不同的釋出者,而且使用起來也有頗多的注意事項,但是這些釋出者無疑是大幅度地提升了我們進行響應式程式設計的效率。
如果將 Combine 與 SwiftUI 結合在一起,我們就可以充分地享受宣告式程式設計帶來的易讀、便利、高效以及優雅。
不過,這就需要我們充分掌握 Combine 和 SwiftUI 中的基礎知識和重難點。否則,一定會有很多坑在等著我們~
最後,除了這些普通的 Publishers,Combine 還為我們提供了特殊的釋出者 —— Subjects。
請閱讀:[Combine 框架,從0到1 —— 5.Combine 中的 Subjects](https://blog.ficowshen.com/page/post/22)
參考內容:
[Using Combine](https://heckj.github.io/swiftui-notes/)
[Combine — share() and multicast()](https://medium.com/jllnmercier/combine-share-and-multicast-dcd75fa7d9d6)