- Swift Combine 學習(一):Combine 初印象
- Swift Combine 學習(二):發布者 Publisher
- Swift Combine 學習(三):Subscription和 Subscriber
- Swift Combine 學習(四):操作符 Operator
- Swift Combine 學習(五):Backpressure和 Scheduler
- Swift Combine 學習(六):自定義 Publisher 和 Subscriber
- Swift Combine 學習(七):實踐應用場景舉例
文章目錄
- 引言
- 操作符 (`Operator`)
- 類型擦除(Type Erasure)
- 結語
引言
在前幾篇文章中,我們已經了解了 Combine 框架的基本概念、發布者和訂閱者的工作機制。本文將詳細介紹 Combine 中的操作符(Operator),這些操作符是處理和轉換數據流的重要工具。通過學習各類操作符的使用,我們可以更靈活地處理異步事件流,構建復雜的數據處理鏈條,從而提升應用的響應能力和性能。
操作符 (Operator
)
Operator
在 Combine 中用于處理、轉換 Publisher
發出的數據。Operator
修改、過濾、組合或以其他方式操作數據流。Combine 提供了大量內置操作符,如:
-
轉換操作符:如
map
、flatMap
和scan
,用于改變數據的形式或結構。-
scan
:用于對上游發布者發出的值進行累加計算。它接收一個初始值和一個閉包,每次上游發布者發出一個新元素時,scan
會根據閉包計算新的累加值,并將累加結果傳遞給下游。let publisher = [1, 2, 3, 4].publisher publisher.scan(0, { a, b ina+b}).sink { print($0) } // 1 3 6 10
-
map
:用于對上游發布者發出的值進行轉換。它接收一個閉包,該閉包將每個從上游發布者接收到的值轉換為新的值,然后將這個新值發給下游let nums = [1, 2, 3, 4, 5] let publisher = nums.publisherpublisher.map { $0 * 10 } // 將每個數乘以10.sink { print($0) }// 輸出: 10 20 30 40 50
-
flatMap
:用于將上游發布者發出的值轉換為另一個發布者,并將新的發布者的值傳遞給下游。與map
不同,它可以對發布者進行展平,消除嵌套。import Combinelet publisher = [[1, 2, 3], [4, 5, 6]].publisher// 使用 flatMap 將每個數組轉換為新的發布者并展平 let cancellable = publisher.flatMap { arr inarr.publisher // 將每個數組轉換為一個新的發布者}.sink { value inprint(value)}/* 輸出: 1 2 3 4 5 6 */
-
-
過濾操作符:包括
filter
、compactMap
和removeDuplicates
,用于選擇性地處理某些數據。let numbers = ["1", "2", nil, "2", "4", "4", "5", "three", "6", "6", "6"] let publisher = numbers.publisherlet subscription = publisher// 使用 compactMap 將字符串轉換為整數。如果轉換失敗就過濾掉該元素.compactMap { $0.flatMap(Int.init) }// filter 過濾掉不符合條件的元素. 如過濾掉小于 3 的數.filter { $0 >= 3 }// 用 removeDuplicates 移除連續重復的元素.removeDuplicates().sink {print($0)}// 輸出: 4 5 6
-
組合操作符:如
merge
、zip
和combineLatest
,用于將多個數據流合并成一個。combineLatest
:用于將多個發布者的最新值合成一個新的發布者。每當任何一個輸入發布者發出新值時,combineLatest
操作符會將每個發布者的最新值組合并作為元組向下游發送。merge
:用于將多個發布者合并為一個單一的發布者,以不確定性的順序發出所有輸入發布者的值。zip
:用于將兩個發布者組合成一個新的發布者,該發布者發出包含每個輸入發布者的最新值的元組。
let numberPublisher = ["1", "2", nil].publisher.compactMap { Int($0 ?? "") } let letterPublisher = ["A", "B", "C"].publisher let extraNumberPublisher = ["10", "20", "30"].publisher.compactMap { Int($0) }// 使用 merge 合并 numberPublisher 和 extraNumberPublisher print("Merge Example:") let mergeSubscription = numberPublisher.merge(with: extraNumberPublisher).sink { value inprint("Merge received: \(value)")}// 使用 zip 將 numberPublisher 和 letterPublisher 配對 print("\n🍎Zip Example🍎") let zipSubscription = numberPublisher.zip(letterPublisher).sink { number, letter inprint("Zip received: number: \(number), letter: \(letter)")}// 使用 combineLatest 將 numberPublisher 和 letterPublisher 的最新值組合 print("\n🍎CombineLatest Example🍎") let combineLatestSubscription = numberPublisher.combineLatest(letterPublisher).sink { number, letter inprint("CombineLatest received: number: \(number), letter: \(letter)")}/*輸出 Merge Example: Merge received: 1 Merge received: 3 Merge received: 10 Merge received: 20 Merge received: 30🍎Zip Example🍎 Zip received: number: 1, letter: A Zip received: number: 3, letter: B🍎CombineLatest Example🍎 CombineLatest received: number: 3, letter: A CombineLatest received: number: 3, letter: B CombineLatest received: number: 3, letter: C */
-
時間相關操作符:例如
debounce
、throttle
和delay
,用于控制數據發送的時機。debounce
:在指定時間窗口內,如果沒有新的事件到達,才會發布最后一個事件。通常用于防止過于頻繁的觸發,比如搜索框的實時搜索。throttle
:在指定時間間隔內,只發布一次。如果latest
為true
,會發布時間段內的最后一個元素,false
時發布第一個元素。delay
:將事件的發布推遲指定時間。
import UIKit import Combine import Foundation import SwiftUIclass ViewController: UIViewController {var cancellableSets: Set<AnyCancellable>?override func viewDidLoad() {super.viewDidLoad()cancellableSets = Set<AnyCancellable>()testDebounce() // testThrottle() // testDelay()}func testDebounce() {print("🍎 Debounce Example 🍎")let searchText = PassthroughSubject<String, Never>()searchText.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main).sink { text inprint("Search request: \(text) at \(Date())")}.store(in: &cancellableSets!)// Simulate rapid input["S", "Sw", "Swi", "Swif", "Swift"].enumerated().forEach { index, text inDispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {print("Input: \(text) at \(Date())")searchText.send(text)}}}// Throttle Examplefunc testThrottle() {print("🍎 Throttle Example 🍎")let scrollEvents = PassthroughSubject<Int, Never>()scrollEvents.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: false).sink { position inprint("Handle scroll position: \(position) at \(Date())")}.store(in: &cancellableSets!)// Simulate rapid scrolling(1...5).forEach { position inprint("Scrolled to: \(position) at \(Date())")scrollEvents.send(position)}}// Delay Examplefunc testDelay() {print("🍎 Delay Example 🍎")let notifications = PassthroughSubject<String, Never>()notifications.delay(for: .seconds(1), scheduler: DispatchQueue.main).sink { message inprint("Display notification: \(message) at \(Date())")}.store(in: &cancellableSets!)print("Send notification: \(Date())")notifications.send("Operation completed")} }/* 🍎 Debounce Example 🍎 輸入: S at 2024-10-21 09:23:19 +0000 輸入: Sw at 2024-10-21 09:23:19 +0000 輸入: Swi at 2024-10-21 09:23:19 +0000 輸入: Swif at 2024-10-21 09:23:19 +0000 輸入: Swift at 2024-10-21 09:23:19 +0000 搜索請求: Swift at 2024-10-21 09:23:19 +0000 */
-
錯誤處理操作符:如
catch
和retry
,用于處理錯誤情況。 -
處理多個訂閱者:例如
multicast
和share
-
multicast
:使用multicast
操作符時,它會將原始的Publisher
包裝成一個ConnectablePublisher
,并且將所有訂閱者的訂閱合并為一個單一的訂閱。這樣,無論有多少個訂閱者,原始的Publisher
都只會收到一次receive(_:)
調用,即對每個事件只處理一次。然后,multicast
操作符會將事件分發給所有的訂閱者。import Combinevar cancelables: Set<AnyCancellable> = Set<AnyCancellable>()let publisher = PassthroughSubject<Int, Never>()// 不使用 multicast() 的情況 let randomPublisher1 = publisher.map { _ in Int.random(in: 1...100) }print("Without multicast():") randomPublisher1.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancelables)randomPublisher1.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancelables)publisher.send(1)let publisher2 = PassthroughSubject<Int, Never>()// 使用 multicast() 的情況 let randomPublisher2 = publisher2.map { _ in Int.random(in: 1...100) }.multicast(subject: PassthroughSubject<Int, Never>())print("\nWith multicast():") randomPublisher2.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancelables)randomPublisher2.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancelables)let connect = randomPublisher2.connect() publisher2.send(1)/*輸出: Without multicast(): Subscriber 1 received: 43 Subscriber 2 received: 39With multicast(): Subscriber 1 received: 89 Subscriber 2 received: 89 */
-
share
:它是一個自動連接的多播操作符,會在第一個訂閱者訂閱時開始發送值,并且會保持對上游發布者的訂閱直到最后一個訂閱者取消訂閱。當多個訂閱者訂閱時,所有訂閱者接收相同的輸出,而不是每次訂閱時重新觸發數據流。import Combinevar cancellables: Set<AnyCancellable> = Set<AnyCancellable>()let publisher = PassthroughSubject<Int, Never>()// 不使用 share() 的情況 let randomPublisher1 = publisher.map { _ in Int.random(in: 1...100)}print("Without share():") randomPublisher1.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancellables)randomPublisher1.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancellables)publisher.send(1)let publisher2 = PassthroughSubject<Int, Never>()// 使用 share() 的情況 let randomPublisher2 = publisher2.map { _ in Int.random(in: 1...100)}.share()print("\nWith share():") randomPublisher2.sink {print("Subscriber 1 received: \($0)")}.store(in: &cancellables)randomPublisher2.sink {print("Subscriber 2 received: \($0)")}.store(in: &cancellables)publisher2.send(1)/* 輸出 Without share(): Subscriber 2 received: 61 Subscriber 1 received: 62With share(): Subscriber 2 received: 92 Subscriber 1 received: 92 */
share
和multicast
的區別:- 自動連接:使用
share
時,原始Publisher
會在第一個訂閱者訂閱時自動連接,并在最后一個訂閱者取消訂閱時自動斷開連接。 - 無需手動連接:無需顯式調用
connect()
方法來啟動數據流,share
會自動管理連接。
-
我們可以使用這些操作符創建成一個鏈條。Operator
通常作為 Publisher
的擴展方法實現。
以下是一個簡化的 map
操作符示例:
extension Publishers {struct Map<Upstream: Publisher, Output>: Publisher {typealias Failure = Upstream.Failurelet upstream: Upstreamlet transform: (Upstream.Output) -> Outputfunc receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {upstream.subscribe(Subscriber(downstream: subscriber, transform: transform))}}
}extension Publisher {func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {return Publishers.Map(upstream: self, transform: transform)}
}
類型擦除(Type Erasure)
類型擦除(type erasure)允許在不暴露具體類型的情況下,對遵循相同協議的多個類型進行統一處理。換句話說,類型擦除可以將不同類型的數據包裝成一個統一的類型,從而實現更靈活、清晰、通用的編程。
let publisher = Just(5).map { $0 * 2 }.filter { $0 > 5 }
在這個簡單的例子中 Publisher 的實際類型是 Publishers.Filter<Publishers.Map<Just<Int>, Int>, Int>
。類型會變得非常復雜,特別是在使用多個操作符連接多個 Publisher
的時候。回到 Combine 中的 AnySubscriber
和 AnyPublisher,每個 Publisher
都有一個方法 eraseToAnyPublisher()
,它可以返回一個 AnyPublisher
實例。就會被簡化為 AnyPublisher<Int, Never>
。
let publisher: AnyPublisher<Int, Never> = Just(5).map { $0 * 2 }.filter { $0 > 5 }.eraseToAnyPublisher() // 使用 eraseToAnyPublisher 方法對 Publisher 進行類型擦除
因為是 Combine 的學習,在此不對類型擦除展開過多。
結語
操作符是 Combine 框架中強大的工具,它們使得數據流的處理和轉換變得更加靈活和高效。通過掌握操作符的使用,開發者可以創建更復雜和功能強大的數據處理邏輯。在下一篇文章中,我們將深入探討 Combine 中的 Backpressure 和 Scheduler,進一步提升對異步數據流的理解和控制調度能力。
- Swift Combine 學習(五):Backpressure和 Scheduler