1. 程式人生 > >Combine 框架,從0到1 —— 5.Combine 常用操作符

Combine 框架,從0到1 —— 5.Combine 常用操作符

  本文首發於 [Ficow Shen's Blog](https://ficowshen.com),原文地址: [Combine 框架,從0到1 —— 5.Combine 常用操作符](https://blog.ficowshen.com/page/post/24)。   ## 內容概覽 - 前言 - print - breakpoint - handleEvents - map - flatMap - eraseToAnyPublisher - merge - combineLatest - zip - setFailureType - switchToLatest - 總結   ## 前言   正所謂,工欲善其事,必先利其器。在開始使用 `Combine` 進行響應式程式設計之前,建議您先了解 `Combine` 為您提供的各種釋出者(Publishers)、操作符(Operators)、訂閱者(Subscribers)。 Combine 操作符(Operators) 其實是釋出者,這些操作符釋出者的值由上游釋出者提供。操作符封裝了很多常用的響應式程式設計演算法,有一些可以幫助我們更輕鬆地進行除錯,而另一些可以幫助我們更輕鬆地通過結合多個操作符來實現業務邏輯,本文將主要介紹這兩大類操作符。 後續示例程式碼中出現的 `cancellables` 均由 `CommonOperatorsDemo` 例項提供: ``` swift final class CommonOperatorsDemo { private var cancellables = Set() } ```   ## print 官網文件:https://developer.apple.com/documentation/combine/publishers/print   `print` 操作符主要用於列印所有釋出的事件,您可以選擇為輸出的內容新增字首。 `print` 會在接收到以下事件時列印訊息: - subscription,訂閱事件 - value,接收到值元素 - normal completion,正常的完成事件 - failure,失敗事件 - cancellation,取消訂閱事件   示例程式碼: ``` swift func printDemo() { [1, 2].publisher .print("_") .sink { _ in } .store(in: &cancellables) } ``` 輸出內容: ``` _: receive subscription: ([1, 2]) _: request unlimited _: receive value: (1) _: receive value: (2) _: receive finished ```   ## breakpoint 官網文件:https://developer.apple.com/documentation/combine/publishers/breakpoint   `breakpoint` 操作符可以傳送除錯訊號來讓偵錯程式暫停程序的執行,只要在給定的閉包中返回 `true` 即可。 示例程式碼: ``` swift func breakpointDemo() { [1, 2].publisher .breakpoint(receiveSubscription: { subscription in return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起 }, receiveOutput: { value in return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起 }, receiveCompletion: { completion in return false // 返回 true 以丟擲 SIGTRAP 中斷訊號,偵錯程式會被調起 }) .sink(receiveValue: { _ in }) .store(in: &cancellables) } ``` 您可能會好奇,為什麼需要用這個操作符來實現斷點,為何不直接打斷點呢? 從上面的示例程式碼中,我們可以看出,通過使用 `breakpoint` 操作符,我們可以很容易地在訂閱操作、輸出、完成發生時啟用斷點。 如果這時候想直接在程式碼上打斷點,我們就要重寫 `sink` 部分的程式碼,而且無法輕易地為訂閱操作啟用斷點。   ## handleEvents 官網文件:https://developer.apple.com/documentation/combine/publishers/handleevents   `handleEvents` 操作符可以在釋出事件發生時執行指定的閉包。 示例程式碼: ``` swift func handleEventsDemo() { [1, 2].publisher .handleEvents(receiveSubscription: { subscription in // 訂閱事件 }, receiveOutput: { value in // 值事件 }, receiveCompletion: { completion in // 完成事件 }, receiveCancel: { // 取消事件 }, receiveRequest: { demand in // 請求需求的事件 }) .sink(receiveValue: { _ in }) .store(in: &cancellables) } ``` `handleEvents` 接受的閉包都是可選型別的,所以我們可以只需要對感興趣的事件進行處理即可,不必為所有引數傳入一個閉包。   ## map 官網文件:https://developer.apple.com/documentation/combine/publishers/map   ![寶石圖](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1600003092139.png) `map` 操作符會執行給定的閉包,將上游釋出的內容進行轉換,然後再發送給下游訂閱者。和 Swift 標準庫中的 `map` 函式類似。 示例程式碼: ``` swift func mapDemo() { [1, 2].publisher .map { $0.description + $0.description } .sink(receiveValue: { value in print(value) }) .store(in: &cancellables) } ``` 輸出內容: ``` 11 22 ```   ## flatMap 官網文件:https://developer.apple.com/documentation/combine/publishers/flatmap   ![寶石圖](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1600002991390.png) `flatMap` 操作符會轉換上游釋出者傳送的所有的元素,然後返回一個新的或者已有的釋出者。 `flatMap` 會將所有返回的釋出者的輸出合併到一個輸出流中。我們可以通過 `flatMap` 操作符的 `maxPublishers` 引數指定返回的釋出者的最大數量。 `flatMap` 常在錯誤處理中用於返回備用釋出者和預設值,示例程式碼: ``` swift 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 in return Just(data) .decode(type: Model?.self, decoder: JSONDecoder()) .catch {_ in // 解析失敗時,返回預設值 nil return Just(nil) }.eraseToAnyPublisher() } .sink(receiveValue: { value in print(value) }) .store(in: &cancellables) } ``` 輸出內容: ``` Optional(CombineDemo.CommonOperatorsDemo.Model(id: 1)) nil Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3)) ``` > 錯誤處理在響應式程式設計中是一個重點內容,也是一個常見的坑!一定要小心,一定要注意!!! 如果沒有 `catch` 操作符,上面的事件流就會因為 `data2` 解析失敗而終止。 比如,現在將 `catch` 去掉: ``` swift [data1, data2, data3].publisher .setFailureType(to: Error.self) .flatMap { data -> AnyPublisher 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) ``` 此時,輸出內容變為了: ``` 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))) ``` 最終,下游訂閱者因為上游發生了錯誤而終止了訂閱,下游便無法收到 `Optional(CombineDemo.CommonOperatorsDemo.Model(id: 3))`。   ## eraseToAnyPublisher 官網文件:https://developer.apple.com/documentation/combine/anypublisher   `eraseToAnyPublisher` 操作符可以將一個釋出者轉換為一個型別擦除後的 `AnyPublisher` 釋出者。 這樣做可以避免過長的泛型型別資訊,比如:`Publishers.Catch, CommonOperatorsDemo.Model?, JSONDecoder>, Just>`。使用 `eraseToAnyPublisher` 操作符將型別擦除後,我們可以得到 `AnyPublisher` 型別。 除此之外,如果需要向呼叫方暴露內部的釋出者,使用 `eraseToAnyPublisher` 操作符也可以對外部隱藏內部的實現細節。 示例程式碼請參考上文 `flatMap` 部分的內容。   ## merge 官網文件:https://developer.apple.com/documentation/combine/publishers/merge   ![寶石圖](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1600002717151.png) `merge` 操作符可以將上游釋出者傳送的元素合併到一個序列中。`merge` 操作符要求上游釋出者的輸出和失敗型別完全相同。 `merge` 操作符有多個版本,分別對應上游釋出者的個數: - merge - merge3 - merge4 - merge5 - merge6 - merge7 - merge8 示例程式碼: ``` swift func mergeDemo() { let oddPublisher = PassthroughSubject() let evenPublisher = PassthroughSubject() 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) } ``` 輸出內容: ``` 1 2 3 4 ```   ## combineLatest 官網文件:https://developer.apple.com/documentation/combine/publishers/combinelatest   ![寶石圖](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1600003220689.png) `combineLatest` 操作符接收來自上游釋出者的最新元素,並將它們結合到一個元組後進行傳送。 `combineLatest` 操作符要求上游釋出者的失敗型別完全相同,輸出型別可以不同。 `combineLatest` 操作符有多個版本,分別對應上游釋出者的個數: - combineLatest - combineLatest3 - combineLatest4 示例程式碼: ``` swift func combineLatestDemo() { let oddPublisher = PassthroughSubject() let evenStringPublisher = PassthroughSubject() 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") } ``` 輸出內容: ``` (1, "2") (3, "2") (3, "4") ``` 請注意,這裡的第一次輸出是 `(1, "2")`,`combineLatest` 操作符的下游訂閱者只有在`所有的上游釋出者都發布了值`之後才會收到結合了的值。   ## zip 官網文件:https://developer.apple.com/documentation/combine/publishers/zip   ![寶石圖](https://ficowblog.oss-cn-shenzhen.aliyuncs.com/uploads/1600003571270.png) `zip` 操作符會將上游釋出者釋出的元素結合到一個流中,在每個上游釋出者傳送的元素配對時才向下遊傳送一個包含配對元素的元組。 `zip` 操作符要求上游釋出者的失敗型別完全相同,輸出型別可以不同。 `zip` 操作符有多個版本,分別對應上游釋出者的個數: - zip - zip3 - zip4 示例程式碼: ``` swift func zipDemo() { let oddPublisher = PassthroughSubject() let evenStringPublisher = PassthroughSubject() 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") } ``` 輸出內容: ``` (1, "2") (3, "4") ``` 請注意,因為 1 和 "2" 可以配對,3 和 "4" 可以配對,所以它們被輸出。而 "6" 和 "8" 無法完成配對,所以沒有被輸出。 和 `combineLatest` 操作符一樣,`zip` 操作符的下游訂閱者只有在`所有的上游釋出者都發布了值`之後才會收到結合了的值。   ## setFailureType 官網文件:https://developer.apple.com/documentation/combine/publishers/setfailuretype   `setFailureType` 操作符可以將當前序列的失敗型別設定為指定的型別,主要用於適配具有不同失敗型別的釋出者。 示例程式碼: ``` swift func setFailureTypeDemo() { let publisher = PassthroughSubject() 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) } ``` 輸出內容: ``` 2 1 ``` 如果註釋 `.setFailureType(to: Error.self)` 這一行程式碼,編譯器就會給出錯誤: `Instance method 'merge(with:)' requires the types 'Never' and 'Error' be equivalent` 因為,`Just(2)` 的失敗型別是 `Never`,而 `PassthroughSubject()` 的失敗型別是 `Error`。 通過呼叫 `setFailureType` 操作符,可以將 `Just(2)` 的失敗型別設定為 `Error`。   ## switchToLatest 官網文件:https://developer.apple.com/documentation/combine/publishers/switchtolatest   `switchToLatest` 操作符可以將來自多個釋出者的事件流展平為單個事件流。 `switchToLatest` 操作符可以為下游提供一個持續的訂閱流,同時內部可以切換多個釋出者。比如,對 `Publisher, Never>` 型別呼叫 `switchToLatest()` 操作符後,結果會變成 `Publisher` 型別。下游訂閱者只會看到一個持續的事件流,即使這些事件可能來自於多個不同的上游釋出者。 下面是一個簡單的示例,可以讓我們更容易理解 `switchToLatest` 到底做了什麼。示例程式碼: ``` swift func switchToLatestDemo() { let subjects = PassthroughSubject, Never>() subjects .switchToLatest() .sink(receiveValue: { print($0) }) .store(in: &cancellables) let stringSubject1 = PassthroughSubject() subjects.send(stringSubject1) stringSubject1.send("A") let stringSubject2 = PassthroughSubject() subjects.send(stringSubject2) // 釋出者切換為 stringSubject2 stringSubject1.send("B") // 下游不會收到 stringSubject1.send("C") // 下游不會收到 stringSubject2.send("D") stringSubject2.send("E") stringSubject2.send(completion: .finished) } ``` 輸出內容: ``` A D E ``` 下面將是一個更復雜但是卻更常見的用法,示例程式碼: ``` swift func switchToLatestDemo2() { let subject = PassthroughSubject() subject.map { value in // 在這裡發起網路請求,或者其他可能失敗的任務 return Future { promise in if let intValue = Int(value) { // 根據傳入的值來延遲執行 DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(intValue)) { print(#function, intValue) promise(.success(intValue)) } } else { // 失敗就立刻完成 promise(.failure(Errors.notInteger)) } } .replaceError(with: 0) // 提供預設值,防止下游的訂閱因為失敗而被終止 .setFailureType(to: Error.self) .eraseToAnyPublisher() } .switchToLatest() .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { value in print(value) }) .store(in: &cancellables) subject.send("3") // 下游不會收到 3 subject.send("") // 立即失敗,下游會收到0,之前的 3 會被丟棄 subject.send("1") // 延時 1 秒後,下游收到 1 } ``` 輸出內容: ``` 0 switchToLatestDemo2() 1 1 switchToLatestDemo2() 3 ``` 請注意,在傳送了 `""` 之後,之前傳送的 `"3"` 依然會觸發 `Future` 中的操作,但是這個 `Future` 裡的 `promise(.success(intValue))` 中傳入的 `3`,下游不會收到。   ## 總結   `Combine` 中還有非常多的預置操作符,如果您感興趣,可以去官網一探究竟:https://developer.apple.com/documentation/combine/publishers 雖然學習這些操作符的成本略高,但是當您掌握之後,開發效率必然會大幅提升。尤其是當 `Combine` 與 `SwiftUI` 以及 `MVVM` 結合在一起使用時,這些學習成本就會顯得更加值得!因為,它們可以幫助您寫出更簡潔、更易讀、更優雅,同時也更加容易測試的程式碼! Ficow 還會繼續更新 `Combine` 系列的文章,後續的內容會講解如何將 `Combine` 與 `SwiftUI` 以及 `MVVM` 結合在一起使用。   推薦繼續閱讀:[Combine 框架,從0到1 —— 5.Combine 中的 Scheduler](https://blog.ficowshen.com/page/post/28)   參考內容: [Using Combine](https://heckj.github.io/swiftui-notes) [The Operators of ReactiveX](http://reactivex.io/documentation/operators.html) [Combine — switchToLatest()](https://medium.com/jllnmercier/combine-switchtolatest-52252aaf38c)