iOS響應式編程Combine——簡介

語言: CN / TW / HK

Combine

通過綁定事件處理(event-progressing)操作符來自定義處理異步事件

總覽

Combine框架提供了一種聲明式的Swift API, 用來隨時處理各種值。這些值可以被當做各種各樣的異步事件。Combine聲明瞭發佈者來公開那些隨時可能變化的值。並且訂閲者接收這些來自發布者的值。

  • Publisher 協議聲明瞭一種可以隨時傳遞一系列值的類型。發佈者有operaters(操作符)根據從上游發佈者收到的值採取行動並重新發布它們。
  • 在發佈者的鏈條終端,會有一個 Subscriber 根據它所接收到的元素來做出反應。發佈者只在訂閲者明確要求時才發佈值。這使你的訂閲者代碼可以控制從與其連接的發佈者那裏接收事件的速度。

多種Foundation類型都是通過發佈者來公開他們的功能,包括 TimerNotificationCenterURLSessionCombine 同時也為符合KVO(鍵值觀察)的所有屬性提供了內置發佈者。

你可以綁定多個發佈者的輸出,並使其交互。例如,你可以訂閲一個輸入框(textField)的發佈者來進行更新操作,用其文本值來執行一段網絡請求。你後續也可以用其他發佈者來執行請求的響應來更新APP。

採用了 Combine 之後,通過集中事件處理和排查問題的技術像嵌套閉包以及基於會話的回調來使你的代碼將會變得更加易讀和維護。

引子

接下來我們來看一個關於《利用Combine來接收和處理事件》的討論。

概述

Combine 提供了一種聲明式的途徑來讓你的APP處理各種事件。跟以前執行多個代理回調或者completionHandler閉包相比,亦可以為一個給定事件源創建一個執行鏈。鏈的每一部分都是由處理從上一步接收到的指定行為和元素的操作符組成的。

想象一下有一個App需要根據一個textField的text來適配一個tableView和collectionView,在APPKit中,每在textfield中鍵入一個字符,都會產生一個Notification,你可以用Combine來對這個Notification進行訂閲。在收到通知後,你可以用操作符來改變事件傳遞的內容和計時,也可以用最後的結果來更新APP的用户界面。

連接Publisher和Subscriber

為了能用Combine來接收textfield的通知,訪問NotificationCenter實例並調用他的publisher(for: object:)方法。這個調用方法攜帶通知名和資源對象,並且會返回一個產生通知元素的發佈者。

let pub = NotificationCenter.default.publisher(for: NSControl.textDidChangeNotification, object: filterField)

Subscriber來接收來自發布者的元素。訂閲者定義了一個關聯類型,Input, 來聲明他所接收的類型。訂閲者同樣也會聲明一個類型,Output,來聲明他產生的元素的類型。發佈者和訂閲者都會定義一種類型,Failure,來代表他們產生或者接收的錯誤。想要將訂閲者對接到發佈者,Output必須和Input必須相匹配,同樣Failure類型也要相匹配.

Combine提供了兩種內嵌的訂閲者,這些訂閲者會自動將屬於他們的發佈者的output和failure。 - sink(receiveCompletion:receiveValue)攜帶兩個閉包。第一個閉包在接收到Subcribers.Completion時執行,這個回調是一個代表着publisher正常結束或者發生錯誤。第二個閉包在他接收到來自publisher的元素時執行。 - assign(to:on:)會立即分發每個從一個既定對象的屬性收到的元素,通過key-path來標示這個屬性。

舉個例子,你可以用sink訂閲者來在發佈者完成時/每次收到元素時進行打印:

let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .sink(receiveCompletion: { print ($0) }, receiveValue: { print ($0) }) sink(receiveCompletion:receiveValue)assign(to:on:)兩者都從他們的publisher那裏請求了大量的元素,這些元素的數量是不受限的。想要控制接收元素的頻率,可以通過實現Subscriber協議來創建你自己的訂閲者。

通過Operators來改變Output類型

前述的sink訂閲者都是在receive閉包中執行他的業務。如果需要通過接收到的元素或者需要維持兩者的調用來執行大量自定義業務,這一點就很煩人了。Combine的先進之處就在於他提供了大量的操作符來處理自定義事件傳遞。

比如, NotificationCenter.Publisher.Output,在接收比如來自textfield的字符串時,它並不是一個非常方便的類型。因為publisher的output本質上來講是一個隨時的元素序列, Combine提供了序列修改操作符,比如 map(_:), flatMap(maxPublishers:_:), 和reduce(_:_:)。這些操作符與他們在Swift標準庫中的對應體相似。

你可以添加一個map(_:)操作符,這個操作符返回一個不同類型,來改變這個發佈者的output。在這個案例中,你可以獲取一個NSTextField類型的通知對象,然後獲取這個textField的stringValue

let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .sink(receiveCompletion: { print ($0) }, receiveValue: { print ($0) })

在發佈者鏈產生了你想要的類型之後,用assign(to:on:)取代sink(receiveCompletion:receiveValue:)。下面的例子懈怠了一個從發佈者鏈接收到的字符串,並且將他們分發給一個自定義ViewModel對象的filterString:

let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel)

自定義發佈者和操作符

您可以使用一個運算符來擴展Publisher實例,該運算符執行否則需要手動編碼的操作。以下是可以使用運算符來改進此事件處理鏈的三種方法:

  • 您可以使用filter(_:)運算符來忽略特定長度的輸入或拒絕非字母數字字符,而不是使用鍵入到文本字段中的任何字符串來更新視圖模型。

  • 如果過濾操作很耗時(例如,如果它查詢一個大型數據庫),您可能需要等待用户停止鍵入。為此,debouce(for:scheduler:options:)運算符允許您設置發佈者發出事件之前必須經過的最短時間段。RunLoop類提供了以秒或毫秒為單位指定時間延遲的便利。

  • 如果結果更新了UI,則可以通過調用receive(on:options:)方法將回調傳遞給主線程。通過將RunLoop類提供的Scheduler實例指定為第一個參數,可以告訴Combine在主運行循環上調用訂閲者。

聲明如下: let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } ) .debounce(for: .milliseconds(500), scheduler: RunLoop.main) .receive(on: RunLoop.main) .assign(to:\MyViewModel.filterString, on: myViewModel)

在需要時取消發佈

發佈者持續發出元素,直到正常結束或失敗。如果你不想再訂閲發佈者,一你一取消訂閲。訂閲者類型可以通過sink(receiveCompletion:receiveValue:)assign(to:on:)來執行Cancellable協議,這個協議提供了取消方法: sub?.cancel() 如果你創建了一個自定義的Subscriber,發佈者會在你第一次訂閲他的時候發送一個 Subscription對象。存儲這個訂閲,並在要結束髮布的時候調用cancel()方法。當你創建了一個自定義的訂閲者,你應該執行Cancellable協議,並且讓cancel()方法來實現存儲的訂閲。