Combine 框架,從0到1 —— 5.Combine 常用操作符
本文首發(fā)于 Ficow Shen's Blog,原文地址: Combine 框架,從0到1 —— 5.Combine 常用操作符。
內(nèi)容概覽
- 前言
- breakpoint
- handleEvents
- map
- flatMap
- eraseToAnyPublisher
- merge
- combineLatest
- zip
- setFailureType
- switchToLatest
- 總結(jié)
前言
正所謂,工欲善其事,必先利其器。在開始使用 Combine 進(jìn)行響應(yīng)式編程之前,建議您先了解 Combine 為您提供的各種發(fā)布者(Publishers)、操作符(Operators)、訂閱者(Subscribers)。
Combine 操作符(Operators) 其實(shí)是發(fā)布者,這些操作符發(fā)布者的值由上游發(fā)布者提供。操作符封裝了很多常用的響應(yīng)式編程算法,有一些可以幫助我們更輕松地進(jìn)行調(diào)試,而另一些可以幫助我們更輕松地通過結(jié)合多個(gè)操作符來(lái)實(shí)現(xiàn)業(yè)務(wù)邏輯,本文將主要介紹這兩大類操作符。
后續(xù)示例代碼中出現(xiàn)的 cancellables 均由 CommonOperatorsDemo 實(shí)例提供:
final class CommonOperatorsDemo {
private var cancellables = Set<AnyCancellable>()
}
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/print
print 操作符主要用于打印所有發(fā)布的事件,您可以選擇為輸出的內(nèi)容添加前綴。
print 會(huì)在接收到以下事件時(shí)打印消息:
- subscription,訂閱事件
- value,接收到值元素
- normal completion,正常的完成事件
- failure,失敗事件
- cancellation,取消訂閱事件
示例代碼:
func printDemo() {
[1, 2].publisher
.print("_")
.sink { _ in }
.store(in: &cancellables)
}
輸出內(nèi)容:
_: receive subscription: ([1, 2])
_: request unlimited
_: receive value: (1)
_: receive value: (2)
_: receive finished
breakpoint
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/breakpoint
breakpoint 操作符可以發(fā)送調(diào)試信號(hào)來(lái)讓調(diào)試器暫停進(jìn)程的運(yùn)行,只要在給定的閉包中返回 true 即可。
示例代碼:
func breakpointDemo() {
[1, 2].publisher
.breakpoint(receiveSubscription: { subscription in
return false // 返回 true 以拋出 SIGTRAP 中斷信號(hào),調(diào)試器會(huì)被調(diào)起
}, receiveOutput: { value in
return false // 返回 true 以拋出 SIGTRAP 中斷信號(hào),調(diào)試器會(huì)被調(diào)起
}, receiveCompletion: { completion in
return false // 返回 true 以拋出 SIGTRAP 中斷信號(hào),調(diào)試器會(huì)被調(diào)起
})
.sink(receiveValue: { _ in
})
.store(in: &cancellables)
}
您可能會(huì)好奇,為什么需要用這個(gè)操作符來(lái)實(shí)現(xiàn)斷點(diǎn),為何不直接打斷點(diǎn)呢?
從上面的示例代碼中,我們可以看出,通過使用 breakpoint 操作符,我們可以很容易地在訂閱操作、輸出、完成發(fā)生時(shí)啟用斷點(diǎn)。
如果這時(shí)候想直接在代碼上打斷點(diǎn),我們就要重寫 sink 部分的代碼,而且無(wú)法輕易地為訂閱操作啟用斷點(diǎn)。
handleEvents
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/handleevents
handleEvents 操作符可以在發(fā)布事件發(fā)生時(shí)執(zhí)行指定的閉包。
示例代碼:
func handleEventsDemo() {
[1, 2].publisher
.handleEvents(receiveSubscription: { subscription in
// 訂閱事件
}, receiveOutput: { value in
// 值事件
}, receiveCompletion: { completion in
// 完成事件
}, receiveCancel: {
// 取消事件
}, receiveRequest: { demand in
// 請(qǐng)求需求的事件
})
.sink(receiveValue: { _ in
})
.store(in: &cancellables)
}
handleEvents 接受的閉包都是可選類型的,所以我們可以只需要對(duì)感興趣的事件進(jìn)行處理即可,不必為所有參數(shù)傳入一個(gè)閉包。
map
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/map

map 操作符會(huì)執(zhí)行給定的閉包,將上游發(fā)布的內(nèi)容進(jìn)行轉(zhuǎn)換,然后再發(fā)送給下游訂閱者。和 Swift 標(biāo)準(zhǔn)庫(kù)中的 map 函數(shù)類似。
示例代碼:
func mapDemo() {
[1, 2].publisher
.map { $0.description + $0.description }
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
}
輸出內(nèi)容:
11
22
flatMap
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/flatmap

flatMap 操作符會(huì)轉(zhuǎn)換上游發(fā)布者發(fā)送的所有的元素,然后返回一個(gè)新的或者已有的發(fā)布者。
flatMap 會(huì)將所有返回的發(fā)布者的輸出合并到一個(gè)輸出流中。我們可以通過 flatMap 操作符的 maxPublishers 參數(shù)指定返回的發(fā)布者的最大數(shù)量。
flatMap 常在錯(cuò)誤處理中用于返回備用發(fā)布者和默認(rèn)值,示例代碼:
struct Model: Decodable {
let id: Int
}
func flatMapDemo() {
guard let data1 = #"{"id": 1}"#.data(using: .utf8),
let data2 = #"{"i": 2}"#.data(using: .utf8),
let data3 = #"{"id": 3}"#.data(using: .utf8)
else { fatalError() }
[data1, data2, data3].publisher
.flatMap { data -> AnyPublisher<CommonOperatorsDemo.Model?, Never> in
return Just(data)
.decode(type: Model?.self, decoder: JSONDecoder())
.catch {_ in
// 解析失敗時(shí),返回默認(rèn)值 nil
return Just(nil)
}.eraseToAnyPublisher()
}
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
}
輸出內(nèi)容:
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 1))
nil
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3))
錯(cuò)誤處理在響應(yīng)式編程中是一個(gè)重點(diǎn)內(nèi)容,也是一個(gè)常見的坑!一定要小心,一定要注意!!!
如果沒有 catch 操作符,上面的事件流就會(huì)因?yàn)?data2 解析失敗而終止。
比如,現(xiàn)在將 catch 去掉:
[data1, data2, data3].publisher
.setFailureType(to: Error.self)
.flatMap { data -> AnyPublisher<Model?, Error> in
return Just(data)
.decode(type: Model?.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
此時(shí),輸出內(nèi)容變?yōu)榱耍?/p>
Optional(CombineDemo.CommonOperatorsDemo.Model(id: 1))
failure(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil)))
最終,下游訂閱者因?yàn)樯嫌伟l(fā)生了錯(cuò)誤而終止了訂閱,下游便無(wú)法收到 Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3))。
eraseToAnyPublisher
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/anypublisher
eraseToAnyPublisher 操作符可以將一個(gè)發(fā)布者轉(zhuǎn)換為一個(gè)類型擦除后的 AnyPublisher 發(fā)布者。
這樣做可以避免過長(zhǎng)的泛型類型信息,比如:Publishers.Catch<Publishers.Decode<Just<JSONDecoder.Input>, CommonOperatorsDemo.Model?, JSONDecoder>, Just<CommonOperatorsDemo.Model?>>。使用 eraseToAnyPublisher 操作符將類型擦除后,我們可以得到 AnyPublisher<Model?, Never> 類型。
除此之外,如果需要向調(diào)用方暴露內(nèi)部的發(fā)布者,使用 eraseToAnyPublisher 操作符也可以對(duì)外部隱藏內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)。
示例代碼請(qǐng)參考上文 flatMap 部分的內(nèi)容。
merge
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/merge

merge 操作符可以將上游發(fā)布者發(fā)送的元素合并到一個(gè)序列中。merge 操作符要求上游發(fā)布者的輸出和失敗類型完全相同。
merge 操作符有多個(gè)版本,分別對(duì)應(yīng)上游發(fā)布者的個(gè)數(shù):
- merge
- merge3
- merge4
- merge5
- merge6
- merge7
- merge8
示例代碼:
func mergeDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenPublisher = PassthroughSubject<Int, Never>()
oddPublisher
.merge(with: evenPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenPublisher.send(2)
oddPublisher.send(3)
evenPublisher.send(4)
}
輸出內(nèi)容:
1
2
3
4
combineLatest
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/combinelatest

combineLatest 操作符接收來(lái)自上游發(fā)布者的最新元素,并將它們結(jié)合到一個(gè)元組后進(jìn)行發(fā)送。
combineLatest 操作符要求上游發(fā)布者的失敗類型完全相同,輸出類型可以不同。
combineLatest 操作符有多個(gè)版本,分別對(duì)應(yīng)上游發(fā)布者的個(gè)數(shù):
- combineLatest
- combineLatest3
- combineLatest4
示例代碼:
func combineLatestDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenStringPublisher = PassthroughSubject<String, Never>()
oddPublisher
.combineLatest(evenStringPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenStringPublisher.send("2")
oddPublisher.send(3)
evenStringPublisher.send("4")
}
輸出內(nèi)容:
(1, "2")
(3, "2")
(3, "4")
請(qǐng)注意,這里的第一次輸出是 (1, "2"),combineLatest 操作符的下游訂閱者只有在所有的上游發(fā)布者都發(fā)布了值之后才會(huì)收到結(jié)合了的值。
zip
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/zip

zip 操作符會(huì)將上游發(fā)布者發(fā)布的元素結(jié)合到一個(gè)流中,在每個(gè)上游發(fā)布者發(fā)送的元素配對(duì)時(shí)才向下游發(fā)送一個(gè)包含配對(duì)元素的元組。
zip 操作符要求上游發(fā)布者的失敗類型完全相同,輸出類型可以不同。
zip 操作符有多個(gè)版本,分別對(duì)應(yīng)上游發(fā)布者的個(gè)數(shù):
- zip
- zip3
- zip4
示例代碼:
func zipDemo() {
let oddPublisher = PassthroughSubject<Int, Never>()
let evenStringPublisher = PassthroughSubject<String, Never>()
oddPublisher
.zip(evenStringPublisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
oddPublisher.send(1)
evenStringPublisher.send("2")
oddPublisher.send(3)
evenStringPublisher.send("4")
evenStringPublisher.send("6")
evenStringPublisher.send("8")
}
輸出內(nèi)容:
(1, "2")
(3, "4")
請(qǐng)注意,因?yàn)?1 和 "2" 可以配對(duì),3 和 "4" 可以配對(duì),所以它們被輸出。而 "6" 和 "8" 無(wú)法完成配對(duì),所以沒有被輸出。
和 combineLatest 操作符一樣,zip 操作符的下游訂閱者只有在所有的上游發(fā)布者都發(fā)布了值之后才會(huì)收到結(jié)合了的值。
setFailureType
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/setfailuretype
setFailureType 操作符可以將當(dāng)前序列的失敗類型設(shè)置為指定的類型,主要用于適配具有不同失敗類型的發(fā)布者。
示例代碼:
func setFailureTypeDemo() {
let publisher = PassthroughSubject<Int, Error>()
Just(2)
.setFailureType(to: Error.self)
.merge(with: publisher)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
publisher.send(1)
}
輸出內(nèi)容:
2
1
如果注釋 .setFailureType(to: Error.self) 這一行代碼,編譯器就會(huì)給出錯(cuò)誤:
Instance method 'merge(with:)' requires the types 'Never' and 'Error' be equivalent
因?yàn)椋?code>Just(2) 的失敗類型是 Never,而 PassthroughSubject<Int, Error>() 的失敗類型是 Error。
通過調(diào)用 setFailureType 操作符,可以將 Just(2) 的失敗類型設(shè)置為 Error。
switchToLatest
官網(wǎng)文檔:https://developer.apple.com/documentation/combine/publishers/switchtolatest
switchToLatest 操作符可以將來(lái)自多個(gè)發(fā)布者的事件流展平為單個(gè)事件流。
switchToLatest 操作符可以為下游提供一個(gè)持續(xù)的訂閱流,同時(shí)內(nèi)部可以切換多個(gè)發(fā)布者。比如,對(duì) Publisher<Publisher<Data, NSError>, Never> 類型調(diào)用 switchToLatest() 操作符后,結(jié)果會(huì)變成 Publisher<Data, NSError> 類型。下游訂閱者只會(huì)看到一個(gè)持續(xù)的事件流,即使這些事件可能來(lái)自于多個(gè)不同的上游發(fā)布者。
下面是一個(gè)簡(jiǎn)單的示例,可以讓我們更容易理解 switchToLatest 到底做了什么。示例代碼:
func switchToLatestDemo() {
let subjects = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
subjects
.switchToLatest()
.sink(receiveValue: { print($0) })
.store(in: &cancellables)
let stringSubject1 = PassthroughSubject<String, Never>()
subjects.send(stringSubject1)
stringSubject1.send("A")
let stringSubject2 = PassthroughSubject<String, Never>()
subjects.send(stringSubject2) // 發(fā)布者切換為 stringSubject2
stringSubject1.send("B") // 下游不會(huì)收到
stringSubject1.send("C") // 下游不會(huì)收到
stringSubject2.send("D")
stringSubject2.send("E")
stringSubject2.send(completion: .finished)
}
輸出內(nèi)容:
A
D
E
下面將是一個(gè)更復(fù)雜但是卻更常見的用法,示例代碼:
func switchToLatestDemo2() {
let subject = PassthroughSubject<String, Error>()
subject.map { value in
// 在這里發(fā)起網(wǎng)絡(luò)請(qǐng)求,或者其他可能失敗的任務(wù)
return Future<Int, Error> { promise in
if let intValue = Int(value) {
// 根據(jù)傳入的值來(lái)延遲執(zhí)行
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(intValue)) {
print(#function, intValue)
promise(.success(intValue))
}
} else {
// 失敗就立刻完成
promise(.failure(Errors.notInteger))
}
}
.replaceError(with: 0) // 提供默認(rèn)值,防止下游的訂閱因?yàn)槭《唤K止
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.switchToLatest()
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
.store(in: &cancellables)
subject.send("3") // 下游不會(huì)收到 3
subject.send("") // 立即失敗,下游會(huì)收到0,之前的 3 會(huì)被丟棄
subject.send("1") // 延時(shí) 1 秒后,下游收到 1
}
輸出內(nèi)容:
0
switchToLatestDemo2() 1
1
switchToLatestDemo2() 3
請(qǐng)注意,在發(fā)送了 "" 之后,之前發(fā)送的 "3" 依然會(huì)觸發(fā) Future 中的操作,但是這個(gè) Future 里的 promise(.success(intValue)) 中傳入的 3,下游不會(huì)收到。
總結(jié)
Combine 中還有非常多的預(yù)置操作符,如果您感興趣,可以去官網(wǎng)一探究竟:https://developer.apple.com/documentation/combine/publishers
雖然學(xué)習(xí)這些操作符的成本略高,但是當(dāng)您掌握之后,開發(fā)效率必然會(huì)大幅提升。尤其是當(dāng) Combine 與 SwiftUI 以及 MVVM 結(jié)合在一起使用時(shí),這些學(xué)習(xí)成本就會(huì)顯得更加值得!因?yàn)椋鼈兛梢詭椭鷮懗龈?jiǎn)潔、更易讀、更優(yōu)雅,同時(shí)也更加容易測(cè)試的代碼!
Ficow 還會(huì)繼續(xù)更新 Combine 系列的文章,后續(xù)的內(nèi)容會(huì)講解如何將 Combine 與 SwiftUI 以及 MVVM 結(jié)合在一起使用。
推薦繼續(xù)閱讀:Combine 框架,從0到1 —— 5.Combine 中的 Scheduler
參考內(nèi)容:
Using Combine
The Operators of ReactiveX
Combine — switchToLatest()

浙公網(wǎng)安備 33010602011771號(hào)