SwiftUI - Combine

前言

“一個隨時間處理數(shù)據(jù)的聲明式的 Swift API。”Combine 蘋果采用的一種函數(shù)響應(yīng)式編程的庫,類似于 RxSwift 。Combine 使用了許多在其他語言和庫中可以找到的相同的函數(shù)響應(yīng)概念,并將Swift的靜態(tài)類型特性應(yīng)用到其解決方案中。

像是 React Native 和 Flutter 這樣的移動端跨平臺方案,由于采用了聲明式 UI 的編寫方式和嚴(yán)格的數(shù)據(jù)流動方向,就能夠大幅減輕開發(fā)者的思考負(fù)擔(dān)。

SwiftUI 很明顯也吸收了這些現(xiàn)代的編程思想,在另一個重量級系統(tǒng)框架 Combine 的協(xié)助下,實(shí)現(xiàn)了單一數(shù)據(jù)源的管理。

響應(yīng)式編程的核心是將所有事件轉(zhuǎn)化成為異步的數(shù)據(jù)流,這剛好就是 Combine 的主要功能。Combine 采用觀察者模式,對應(yīng)多個觀察者,可以分別訂閱感興趣的內(nèi)容。在 SwiftUI 的界面布局過程中,不同的 View 就是觀察者,分別訂閱了相關(guān)聯(lián)的屬性,并在數(shù)據(jù)發(fā)生變化之后就能夠自動的重新渲染。

1、Pulishers、Operators、Subscribers

Pulishers:發(fā)布者,負(fù)責(zé)提供數(shù)據(jù)(當(dāng)數(shù)據(jù)可用且獲得請求)。一個發(fā)布者如果沒有訂閱,則不會發(fā)布任何數(shù)據(jù)。當(dāng)你在描述一個發(fā)布者時,你會用兩種相關(guān)類型(associatedtype)來表述他:OutputFailure 。比如發(fā)布者返回 String實(shí)例 ,并且可能以 URLError實(shí)例 的形式返回失敗,那么發(fā)布者可以用 <String, URLError> 來描述。

Subscribers:訂閱者,負(fù)責(zé)(向發(fā)布者)請求數(shù)據(jù)和接收發(fā)布者提供的數(shù)據(jù)(或者失敗信息)。訂閱者用兩種相關(guān)類型進(jìn)行描述:InputFailure 。訂閱者發(fā)起數(shù)據(jù)請求,并空值接收到的數(shù)據(jù)量。在 Combine 中,他可以看作是“行為的驅(qū)動者”,沒有了訂閱者,其他的組成部分將閑置。

發(fā)布者和訂閱者是相互連接的,并構(gòu)成 Combine 的核心。當(dāng)你連接一個訂閱者到發(fā)布者上,Input 和 Output 類型必須一致,兩者的 Failure 也需要一致。

Operators:操作者是一個行為類似訂閱者和發(fā)布者的對象。他既實(shí)現(xiàn)了 Publisher協(xié)議 ,又實(shí)現(xiàn)了 Subscriber協(xié)議 。他們支持訂閱一個發(fā)布者,并接收訂閱者的請求。

三者關(guān)系

一般的數(shù)據(jù)流是這樣處理的:發(fā)布者 -> 操作者1 -> 操作者2 -> ... -> 操作者n -> 訂閱者

操作者可以被用來轉(zhuǎn)換數(shù)值或者值的類型 -- Output 和 Failure 均可。操作者也可以分割、復(fù)制、合并數(shù)據(jù)流。操作者之間的 Output/Failure類型 必須一致,否則編譯器會報錯。

2、Future、Promise

Future:未來某個時刻會發(fā)布一個數(shù)據(jù),會立即結(jié)束,并且會帶有一個狀態(tài),是成功還是失敗的狀態(tài)。(類似我們Swift中的逃逸閉包

final public class Future<Output, Failure> : Publisher where Failure : Error {

    public typealias Promise = (Result<Output, Failure>) -> Void

    public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)

    final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}

查看源碼,包含一個Promise類型及一個逃逸閉包的初始化函數(shù)。Future和Promise結(jié)合使用,一個未來要給的承諾,也就是未來執(zhí)行的操作返回的一個最終結(jié)果。初始化函數(shù)中可以看出Promise為接收單個Result類型的閉包。

拓展:原理上Future和PassthroughSubject、CurrentValueSubject很類似,F(xiàn)uture遵循Publisher協(xié)議,后兩者遵循的是Subject協(xié)議,可以直接使用send方法發(fā)送數(shù)據(jù)。

3、簡單示例

創(chuàng)建一個Future類型的閉包任務(wù)(發(fā)布者),即一個將會在未來某時刻調(diào)用的閉包,閉包會返回字符串3,沒有錯誤返回,:

let futurePublisher = Future<String, Never> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        promise(.success("3"))
    }
}

新增ViewModel數(shù)據(jù)管理類,遵循ObservableObject協(xié)議,即被@Published修飾符修飾的屬性title改變時,就會發(fā)布通知給使用到此屬性的View刷新。

extension ContentView {
    class ViewModel: ObservableObject {
        private var cancellables = Set<AnyCancellable>()
        //刷新視圖用的變量
        @Published var title: String = "Hello Lcr"
        
        func fetchData() {
            futurePublisher.print("_fetchData_")
                .receive(on: RunLoop.main)
                .sink { completion in
                    switch completion {
                    case .failure(let err):
                        print("Error is \(err.localizedDescription)")
                    case .finished:
                        print("Finished")
                    }
                } receiveValue: { [weak self] data in
                    print("fetchWebData: \(data)")
                    self?.title = data
                }
                .store(in: &cancellables)

        }
    }
}

關(guān)于futurePublisher.sink{} receiveValue{}函數(shù),就是在訂閱者和發(fā)布者之間橋梁,以及后續(xù)接收數(shù)據(jù)操作。

Use Publisher/sink(receiveCompletion:receiveValue:) to observe values received by the publisher and process them using a closure you specify.

訂閱者可以通過sink函數(shù)響應(yīng)調(diào)用,block區(qū)域?qū)盏絧ublisher發(fā)出的values,publisher可以發(fā)射0個或多個values,除了基本值之外,您的publisher還會發(fā)給訂閱者特殊值。如.finished(完成)、.failure()(失?。?/p>

struct ContentView: View {
    @StateObject var vm = ViewModel()
    var body: some View {
        Text(vm.title).padding().onAppear{
            vm.fetchData()
        }
    }
}

訂閱者Text通過vm操作者去向發(fā)布者索要數(shù)據(jù),futurePublisher閉包會執(zhí)行,2秒后將Promise閉包執(zhí)行將數(shù)據(jù)返回,receiveValue接收到數(shù)據(jù),保存至cancellables,狀態(tài)為finished,即任務(wù)到此結(jié)束。


combine簡單示例
4、backPresssure

對于大多數(shù)響應(yīng)式編程場景而言,訂閱者不需要對發(fā)布過程進(jìn)行過多的控制。當(dāng)發(fā)布者發(fā)布元素時,訂閱者只需要無條件地接收即可。但是,如果發(fā)布者發(fā)布的速度過快,而訂閱者接收的速度又太慢,我們該怎么解決這個問題呢?Combine 已經(jīng)為我們制定了穩(wěn)健的解決方案!現(xiàn)在,讓我們來了解如何施加背壓(back pressure,也可以叫反壓)以精確控制發(fā)布者何時生成元素。

在 Combine 中,發(fā)布者生成元素,而訂閱者對其接收的元素進(jìn)行操作。不過,發(fā)布者會在訂閱者連接和獲取元素時才發(fā)送元素。訂閱者通過 Subscribers.Demand 類型來表明自己可以接收多少個元素,以此來控制發(fā)布者發(fā)送元素的速率。

訂閱者可以通過兩種方式來表明需求(Demand):

  • 調(diào)用 Subscription 實(shí)例(由發(fā)布者在訂閱者進(jìn)行第一次訂閱時提供)的 request(_:) 方法;
  • 在發(fā)布者調(diào)用訂閱者的 receive(_:) 方法來發(fā)送元素時,返回一個新的 Subscribers.Demand 實(shí)例;

下面利用一個簡單例子演示一下:

let width = UIScreen.main.bounds.width, height = UIScreen.main.bounds.height
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton.init(frame: CGRect.init(x: (width-180)/2, y: 420, width: 180, height: 40))
        button.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside)
        button.setTitle("訂閱 timerPublisher", for: .normal)
        button.backgroundColor = .orange
        button.layer.cornerRadius = 10
        
        view.addSubview(button)
    }
    
    @objc func tapped(button: UIButton) {
        // 訂閱
        print ("開啟訂閱 \(Date())")
        timerPub.subscribe(MySubscriber())
    }
}

// 發(fā)布者: 使用一個定時器來每秒發(fā)送一個日期對象
let timerPub = Timer.publish(every: 1, on: .main, in: .default).autoconnect()

// 訂閱者: 在訂閱以后,等待2秒,然后請求最多3個值
class MySubscriber: Subscriber {
//    typealias Input = Date
//    typealias Failure = Never
//    var subscription: Subscription?
    
    func receive(subscription: Subscription) {
        print("訂閱接收到了")
//        self.subscription = subscription
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            subscription.request(.max(3))
        }
    }
    
    func receive(_ input: Date) -> Subscribers.Demand {
        print("發(fā)布時間:\(input)——————接收時間:\(Date())")
        return Subscribers.Demand.none
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("完成")
    }
}


struct ContentView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> ViewController {
        return ViewController()
    }
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
后壓結(jié)果

可見訂閱者通過 Subscribers.Demand 類型來表明自己可以接收多少個元素,以此來控制發(fā)布者發(fā)送元素的速率。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容