Combine 框架,從0到1 —— 5.Combine 提供的釋出者(Publishers)

Ficow發表於2020-09-25

 

本文首發於 Ficow Shen's Blog,原文地址: Combine 框架,從0到1 —— 5.Combine 提供的釋出者(Publishers)

 

內容概覽

  • 前言
  • Just
  • Future
  • Deferred
  • Empty
  • Publishers.Sequence
  • Fail
  • Record
  • Share
  • Multicast
  • ObservableObject
  • @Published
  • 總結

 

前言

 

正所謂,工欲善其事,必先利其器。在開始使用 Combine 進行響應式程式設計之前,建議您先了解 Combine 為您提供的各種釋出者(Publishers)、操作符(Operators)、訂閱者(Subscribers)。合理地選擇符合需求的 Combine 釋出者,可以大幅度地提升您的開發效率!

這些都是 Combine 為我們提供的釋出者:
JustFutureDeferredEmptyPublishers.SequenceFailRecordShareMulticastObservableObject@Published

接下來的幾分鐘,讓我們把它們各個擊破!

 

請注意,後續內容中出現的 cancellables 全部由這個類的例項提供 :

final class CombinePublishersDemo {

    private var cancellables = Set<AnyCancellable>()

}

 

示例程式碼 Github 倉庫:CombinePublishersDemo

 

Just

官網文件

 

Just 向每個訂閱者只傳送單個值,然後結束。它的失敗型別為 Never,也就是不能失敗。 示例程式碼:

func just() {
        Just(1) // 直接傳送1
            .sink { value in
                // 輸出:just() 1
                print(#function, value)
            }
            .store(in: &cancellables)
}

輸出內容:

just() 1

 

Just 常被用在錯誤處理中,在捕獲異常後傳送備用值。 示例程式碼:

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

官網文件

 

Future 使用一個閉包來進行初始化,最終這個閉包將執行傳入的一個閉包引數(promise)來傳送單個值或者失敗。請不要使用 Future 傳送多個值。PassthroughSubject, CurrentValueSubject 或者 Deferred 會是更好的選擇。

請注意,Future 不會等待訂閱者傳送需求,它會在被建立時就立刻非同步執行這個初始化時傳入的閉包!如果你需要等待訂閱者傳送需求時才執行這個閉包,請使用 Deferred。如果你需要重複執行這個閉包,也請使用 Deferred

示例程式碼:

func future() {
        Future<Int, Never> { 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 作為一個任務函式的返回值,讓具體任務的執行程式碼與訂閱程式碼分離:

private func bigTask() -> Future<Int, Error> {
        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

官網文件

 

Deferred 使用一個生成釋出者的閉包來完成初始化,這個閉包會在訂閱者執行訂閱操作時才執行。

示例程式碼:

func deferred() {
        let deferredPublisher = Deferred<AnyPublisher<Bool, Error>> {
            // 在訂閱之後才會執行
            print(Date(), "Future inside Deferred created")
            return Future<Bool, Error> { 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

官網文件

 

Empty 是一個從不釋出任何值的釋出者,可以選擇立即完成(Empty() 或者 Empty(completeImmediately: true))。

可以使用 Empty(completeImmediately: false) 建立一個從不釋出者(一個從不傳送值,也從不完成或失敗的釋出者)。

Empty 常用於錯誤處理。當錯誤發生時,如果你不想傳送錯誤,可以用 Empty 來傳送完成。

 

示例程式碼:

func empty() {
        Empty<Never, Error>() // 或者 Empty<Never, Error>(completeImmediately: true)
            .sink(receiveCompletion: { completion in
                // 輸出:empty() finished
                print(#function, completion)
            }, receiveValue: { _ in

            })
            .store(in: &self.cancellables)
    }

輸出內容:

empty() finished

 

Publishers.Sequence

官網文件

 

Publishers.Sequence 是傳送一個元素序列的釋出者,元素髮送完畢時會自動傳送完成。

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

官網文件

 

Fail 是一個以指定的錯誤終止序列的釋出者。通常用於返回錯誤,比如:在校驗引數缺失或錯誤等場景中,返回一個 Fail

示例程式碼:

func fail() {
        Fail<Never, NSError>(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

官網文件

 

Record 釋出者允許錄製一系列的輸入和一個完成,錄製之後再傳送給每一個訂閱者。

示例程式碼:

func record() {
        Record<Int, Never> { 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

官網文件

 

Share 釋出者可以和多個訂閱者共享上游釋出者的輸出。請注意,它和其他值型別的釋出者不一樣,這是一個引用型別的釋出者!

當您需要使用引用語義的釋出者時,可以考慮使用這個型別。

為了更好地理解 Share 的意義和用途, 讓我們先來觀察沒有 Share 會出現什麼問題:

func withoutShare() {
        let deferred = Deferred<Future<Int, Never>> {
            print("creating Future")
            return Future<Int, Never> { 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 來嘗試解決這個問題:

func withShare() {
        let deferred = Deferred<Future<Int, Never>> {
            print("creating Future")
            return Future<Int, Never> { 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 控制何時釋出 的內容,我們可以通過自行呼叫 connect() 來解決這個問題。

這是調整後的程式碼:

func withShareAndConnectable() {
        let deferred = Deferred<Future<Int, Never>> {
            print("creating Future")
            return Future<Int, Never> { 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

官網文件

 

Multicast 釋出者使用一個 Subject 向多個訂閱者傳送元素。和 Share 一樣,這也是一個引用型別的釋出者。在使用多個訂閱者進行訂閱時,它們可以有效地保證上游釋出者不重複執行繁重的耗時操作。

而且 Multicast 是一個 ConnectablePublisher,所以我們需要在訂閱者準備就緒之後去手動呼叫 connect() 方法,然後訂閱者才能收到上游釋出者傳送的元素。

示例程式碼:

func multicast() {
        let multicastSubject = PassthroughSubject<Int, Never>()
        let deferred = Deferred<Future<Int, Never>> {
            print("creating Future")
            return Future<Int, Never> { 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

官網文件

 

ObservableObject 是具有釋出者的一種物件,該物件在更改物件之前發出變動元素。常用在 SwiftUI 中。

遵循 ObservableObject 協議的物件會自動生成一個 objectWillChange 釋出者,這個釋出者會在這個物件的 @Published 屬性發生變動時傳送 變動之前的舊值

來自官網文件的示例程式碼:

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

官網文件

 

@Published 是一個屬性包裝器(@propertyWrapper),它可以為任何屬性新增一個 Combine 釋出者。常用在 SwiftUI 中。

請注意,@Published 釋出的是屬性觀察器 willSet 中接收到的新值,但是這個屬性當前的值還是舊值!觀察下面的例子,可以幫助您理解這個重點。

來自官網文件的示例程式碼:

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

 

參考內容:
Using Combine
Combine — share() and multicast()

 

相關文章