摘自《SwiftUI和Combine編程》---《Publisher 和常見 Operator》
Publisher 詳解
Publisher 在接收到訂閱促王,并且接受到請求要求提供若干個事件后鳖擒,才開始對外發(fā)布事件暑脆。接受訂閱和接受請求,也是 Publisher 生命周期的一部分呵扛。
Empty
Empty<Int, SampleError>()
是一個最簡單的 Publisher狡门,它只在被訂閱的時候發(fā)布一個完成事件 (receive finished)舞竿。這個 publisher 不會輸出任何的 output 值,只能用于表示某個事件已經(jīng)發(fā)生奉芦。
/// A publisher that never publishes any values, and optionally finishes immediately.
///
/// You can create a ”Never” publisher — one which never sends values and never finishes or fails — with the initializer `Empty(completeImmediately: false)`.
public struct Empty<Output, Failure> : Publisher, Equatable where Failure : Error
Just
Just(1)
表示一個單一的值赵抢,在被訂閱后,這個值會被發(fā)送出去声功,緊接著是 finished
/// A publisher that emits an output to each subscriber just once, and then finishes.
///
/// You can use a ``Just`` publisher to start a chain of publishers. A ``Just`` publisher is also useful when replacing a value with ``Publishers/Catch``.
public struct Just<Output> : Publisher
Publisher.Sequence
如果我們對一連串的值感興趣的話烦却,可以使用的是 Publishers.Sequence。顧名思義先巴,Publishers.Sequence 接受一個 Sequence:它可以是一個數(shù)組其爵,也可以是一個 Range。在被訂閱時伸蚯,Sequence 中的元素被逐個發(fā)送出來
Publisher.Sequence<[Int], Never>(sequence: [1,2,3])
//等價于
[1,2,3].publisher
常見 Operator
map
元素變形:map 操作可以為我們完成事件中數(shù)據(jù)類型的轉(zhuǎn)換
[1,2,3].publisher.map { $0*2 }
compactMap
將 map 結(jié)果中那些 nil 的元素去除掉醋闭,這個操作通常會“壓縮”結(jié)果,讓其中的元素數(shù)減少朝卒,這也正是其名字中 compact 的來源
flatMap
展平 降維
map 及 compactMap 的閉包返回值是單個的 Output 值证逻。而與它們不同,flatMap 的變形閉包里需要返回一個 Publisher。也就是說囚企,flatMap 將會涉及兩個 Publisher:一個是 flatMap 操作本身所作用的外層 Publisher丈咐,一個是 flatMap 所接受的變形閉包中返回的內(nèi)層 Publisher。flatMap 將外層 Publisher 發(fā)出的事件中的值傳遞給內(nèi)層 Publisher龙宏,然后匯總內(nèi)層 Publisher 給出的事件輸出棵逊,作為最終變形后的結(jié)果。
check("Flat Map 1") {
[[1, 2, 3], ["a", "b", "c"]]
.publisher
.flatMap {
$0.publisher
}
}
// Output:
// 1 2 3 a b c
/**
在被訂閱后银酗,這個外層 Publisher 會發(fā)送兩個 Output 事件 (兩個事件的值分別是 [1, 2, 3] 和 ["a", "b", "c"])辆影,每個事件的值被 flatMap 傳遞到內(nèi)層,并通過 $0.publisher 生成新的 Publisher 并返回黍特。內(nèi)層 Publisher 實際上是 [1, 2, 3].publisher 和 ["a", "b", "c"].publisher蛙讥,它們發(fā)送的值將被作為 flatMap 的結(jié)果,被“展平” (flatten) 后發(fā)送出去灭衷。
*/
check("Flat Map 2") {
["A", "B", "C"]
.publisher
.flatMap { letter in
[1, 2, 3]
.publisher
.map { "\(letter)\($0)" }
}
}
// Output:
// A1 A2 A3 B1 B2 B3 C1 C2 C3
/**
外層 ["A", "B", "C"] 里的每個元素次慢,作為 flatMap 變形閉包的輸入,參與到了內(nèi)層 Publisher 的 map 計算翔曲。內(nèi)層 Publisher 逐次使用 [1, 2, 3] 里的元素迫像,和輸入進來的 letter (也就是 “A”, “B” 和 “C”) 進行拼接后作為新事件發(fā)出。
*/
reduce
操作完發(fā)消息
將數(shù)組中的元素按照某種規(guī)則進行合并瞳遍,并得到一個最終的結(jié)果闻妓。
與 scan 區(qū)別:當序列中值耗盡時,它將發(fā)布 finished掠械。而經(jīng)過 reduce 變形后由缆,新的 Publisher 只會在接到上游發(fā)出的 finished 事件后,才會將 reduce 后的結(jié)果發(fā)布出來份蝴。而緊接這個結(jié)果犁功,則是新的 reduce Publisher 的結(jié)束事件。
scan
每一步都發(fā)消息
場景:在某個下載任務(wù)執(zhí)行期間婚夫,接受 URLSession 的數(shù)據(jù)回調(diào)浸卦,將已接收到的數(shù)據(jù)量做累加來提供一個下載進度條的界面。
同 reduce案糙,但是會記錄中途每一步的過程限嫌。
與 reduce 區(qū)別:一邊進行重復(fù)操作,一邊將每一步中間狀態(tài)發(fā)送出去时捌。
源碼類似: 可直接添加到 Sequence extension 中
extension Sequence {
public func scan<ResultElement>(_ initial: ResultElement, _ nextPartialResult: (ResultElement, Element) -> ResultElement) -> [ResultElement] {
var result: [ResultElement] = []
for x in self {
result.append(nextPartialResult(result.last ?? initial, x))
}
return result
}
}
removeDuplicates
移除連續(xù)出現(xiàn)的重復(fù)事件值
removeDuplicates 經(jīng)常被用來減少那些非常消耗資源的操作怒医,比如由事件觸發(fā)造成的網(wǎng)絡(luò)請求或者圖片渲染。如果當作為源頭的數(shù)據(jù)沒有改變時奢讨,所預(yù)期得到的結(jié)果也不會變化的話稚叹,那么就沒有必要去重復(fù)這樣操作。在源頭將重復(fù)的事件移除,可以讓下游的事件流也變得簡單扒袖。
check("Remove Duplicates") {
// removeDuplicates 會處理事件源
["S", "Sw", "Sw", "Sw", "Swi",
"Swif", "Swift", "Swift", "S"]
.publisher
.removeDuplicates()
}
// Output:
// subscription: (["S", "Sw", "Swi", "Swif", "Swift", "S"])
錯誤處理
Fail
Fail 這個內(nèi)建的基礎(chǔ) Publisher塞茅,它所做的事情就是在被訂閱時發(fā)送一個錯誤事件.
Fail<Int, SampleError>(error: .sampleError)
public enum SampleError: Error {
case sampleError
}
mapError
如果 Publisher 在出錯時發(fā)送的是 SampleError,但訂閱方聲明只接受 MyError 時季率,就算實際上 Publisher 只發(fā)出 Output 值而從不會發(fā)出 Failure 值野瘦,我們也無法使用這個 Subscriber 去接收一個類型不符的 Publisher 的事件。
在這種情況下飒泻,我們可以通過使用 mapError 來將 Publisher 的 Failure 轉(zhuǎn)換成 Subscriber 所需要的 Failure 類型
Fail<Int, SampleError>(error: .sampleError)
.mapError { _ in
myError.myError
}
拋出錯誤
Combine 為 Publisher 的 map 操作提供了一個可以拋出錯誤的版本鞭光,tryMap。使用 tryMap 我們就可以將這類處理數(shù)據(jù)時發(fā)生的錯誤轉(zhuǎn)變?yōu)闃酥臼录魇〉慕Y(jié)束事件泞遗。
["1", "2", "S", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw MyError.myError
}
return value
}
除了 tryMap 以外惰许,Combine 中還有很多類似的以 try 開頭的 Operator,比如 tryScan刹孔,tryFilter啡省,tryReduce 等等娜睛。當你有需求在數(shù)據(jù)轉(zhuǎn)換或者處理時髓霞,將事件流以錯誤進行終止,都可以使用對應(yīng)操作的 try 版本來進行拋出畦戒,并在訂閱者一側(cè)接收到對應(yīng)的錯誤事件方库。
從錯誤中恢復(fù)
如果我們想要在事件流以錯誤結(jié)束時被轉(zhuǎn)為一個默認值的話,replaceError 就會很有用障斋。
replaceError 會將 Publisher 的 Failure 類型抹為 Never纵潦,這正是我們使用 assign 來將 Publisher 綁定到 UI 上時所需要的 Failure 類型。我們可以用 replaceError 來提供這樣一個在出現(xiàn)錯誤時應(yīng)該顯示的默認值垃环。
有一些 Operator 是專門幫助事件流從錯誤中恢復(fù)的邀层,最簡單的是 replaceError,它會把錯誤替換成一個給定的值遂庄,并且立即發(fā)送 finished 事件
replaceError(中斷信號)
["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw MyError.myError
}
return value
}
.replaceError(with: -1)
// Output:
// 1 2 -1
catch(中斷信號)
catch 則略有不同寥院,它接受的是一個新的 Publisher,當上游 Publisher 發(fā)生錯誤時涛目,catch 操作會使用新的 Publisher 來把原來的 Publisher 替換掉秸谢。
["1", "2", "Swift", "4"].publisher
.tryMap { s -> Int in
guard let value = Int(s) else {
throw MyError.myError
}
return value
}
.catch { _ in [-1, -2, -3].publisher }
// Output:
// 1 2 -1 -2 -3
// 當錯誤發(fā)生后,原本的 Publisher 事件流將被中斷霹肝,取而代之估蹄,則是由 catch 所提供的事件流繼續(xù)向后續(xù)的 Operator 及 Subscriber 發(fā)送事件。原來 Publisher 中的最后一個元素 “4”沫换,將沒有機會到達臭蚁。
組合實現(xiàn)繼續(xù)輸入
如果我們將 (由 ["1", "2", "Swift", "4"] 構(gòu)成的) 原 Publisher 看作是用戶輸入,將結(jié)果的 Int 看作是最后輸出,那么像上面那樣的方式使用 replaceError 或者 catch 的話垮兑,一旦用戶輸入了不能轉(zhuǎn)為 Int 的非法值 (如 “Swift”)炭晒,整個結(jié)果將永遠停在我們給定的默認恢復(fù)值上,接下來的任意用戶輸入都將被完全忽略甥角。這往往不是我們想要的結(jié)果网严,一般情況下,我們會想要后續(xù)的用戶輸入也能繼續(xù)驅(qū)動輸出嗤无,這時候我們可以靠組合一些 Operator 來完成所需的邏輯
check("Catch and Continue") {
["1", "2", "Swift", "4"].publisher
.flatMap { s in
return Just(s)
.tryMap { s -> Int in
guard let value = Int(s) else {
throw MyError.myError
}
return value
}
.catch { _ in
Just(-1)}
}
}
// Output:
// 1 2 -1 4
eraseToAnyPublisher
類型抹消
let a = [[1,2,3],[4,5,6]].publisher.flatMap{ $0.publisher }
// 等價于
let a1 = Publishers.FlatMap(upstream: [[1,2,3],[4,5,6]].publisher, maxPublishers: .unlimited) {
$0.publisher
}
let b = a.map {$0*2}
// 此時 b 的類型為: Publishers.Map<Publishers.FlatMap<Publishers.Sequence<[Int], Never>, Publishers.Sequence<[[Int]], Never>>, Int>
Combine 提供了 eraseToAnyPublisher 來幫助我們對復(fù)雜類型的 Publisher 進行類型抹消震束,這個方法返回一個 AnyPublisher。
let c = b.eraseToAnyPublisher()
// c: AnyPublisher<Int, Never>
Combine 中的其他角色也大都提供了類似的抹消后的類型当犯,比如 AnySubscriber 和 AnySubject 等垢村。在大多數(shù)情況下我們都只會關(guān)注某個部件所扮演的角色,也即嚎卫,它到底是一個 Publisher 還是一個 Subscriber嘉栓。一般我們并不關(guān)心具體的類型,因為對 Publisher 的變形往往都伴隨著類型的變化拓诸。通過類型抹消侵佃,可以讓事件的傳遞和訂閱操作變得更加簡單,對外的 API 也更加穩(wěn)定奠支。
操作符熔合
將操作符的作用時機提前到創(chuàng)建 Publisher 時的方式馋辈,被稱為操作符熔合 (operator fusion)。
有時候 map 操作的返回結(jié)果的類型并不是 Publishers.Map倍谜,比如下面的兩個例子:
[1, 2, 3].publisher.map { $0 * 2 }
// Publishers.Sequence<[Int], Never>
Just(10).map { String($0) }
// Just<String>
這是由于 Publishers.Sequence 和 Just 在各自的擴展中對默認的 Publisher 的 map 操作進行了重寫迈螟。由于 Publishers.Sequence 和 Just 這樣的類型在編譯期間我們就能確定它們在被訂閱時就會同步地發(fā)送所有事件,所以可以將 map 的操作直接作用在輸入上尔崔,而不需要等待每次事件發(fā)生時再去操作答毫。
可以想象 Publishers.Sequence 上 map 操作的實現(xiàn):
extension Publishers.Sequence {
public func map<T>(_ transform: (Elements.Element) -> T) -> Publishers.Sequence<[T], Failure> {
return Publishers.Sequence(sequence: sequence.map(transform))
}
}
這樣做可以避免 transform 的 @escaping 的要求,避免存儲這個變形閉包季春,讓所得到的 Publisher 更加高效洗搂,同時也讓后續(xù) Publisher 變形類型能簡單一些。這些優(yōu)點在多次變形的時候尤為突出鹤盒。