在具體介紹 Combine 之前百炬,有兩個(gè)重要的概念需要簡(jiǎn)要介紹一下:
- 觀察者模式
- 響應(yīng)式編程
觀察者模式
觀察者模式(Observer Pattern)是一種設(shè)計(jì)模式贺归,用來描述一對(duì)多關(guān)系:一個(gè)對(duì)象發(fā)生改變時(shí)將自動(dòng)通知其他對(duì)象屋群,其他對(duì)象將相應(yīng)做出反應(yīng)盹愚。這兩類對(duì)象分別被稱為被 觀察目標(biāo)(Observable)和 觀察者(Observer)饲握,也就是說一個(gè)觀察目標(biāo)可以對(duì)應(yīng)多個(gè)觀察者煌珊,觀察者可以訂閱它們感興趣的內(nèi)容号俐,當(dāng)觀察目標(biāo)內(nèi)容改變時(shí),它會(huì)向這些觀察者廣播通知(調(diào)用 Observer 的更新方法)定庵。有一點(diǎn)需要說明的是吏饿,觀察者之間彼此時(shí)互相獨(dú)立的,也并不知道對(duì)方的存在蔬浙。
在 Swift 中猪落,一個(gè)簡(jiǎn)單的觀察者模式可以被描述為:
protocol Observable {
associatedtype T: Observer
mutating func attach(observer: T)
}
protocol Observer {
associatedtype State
func notify(_ state: State)
}
struct AnyObservable<T: Observer>: Observable{
var state: T.State {
didSet {
notifyStateChange()
}
}
var observers: [T] = []
init(_ state: T.State) {
self.state = state
}
mutating func attach(observer: T) {
observers.append(observer)
observer.notify(state)
}
private func notifyStateChange() {
for observer in observers {
observer.notify(state)
}
}
}
struct AnyObserver<S>: Observer {
private let name: String
init(name: String) {
self.name = name
}
func notify(_ state: S) {
print("\(name)'s state updated to \(state)")
}
}
var observable = AnyObservable<AnyObserver<String>>("hello")
let observer = AnyObserver<String>(name: "My Observer")
observable.attach(observer: observer)
observable.state = "world"
// "My Observer's state updated: hello"
// "My Observer's state updated: world"
響應(yīng)式編程
響應(yīng)式編程(Reactive Programming)是一種編程思想,相對(duì)應(yīng)的也有面向過程編程畴博、面向?qū)ο缶幊瘫考伞⒑瘮?shù)式編程等等。不同的是俱病,響應(yīng)式編程的核心是面向異步數(shù)據(jù)流和變化的官疲。
在現(xiàn)在的前端世界中,我們需要處理大量的事件亮隙,既有用戶的交互途凫,也有不斷的網(wǎng)絡(luò)請(qǐng)求,還有來自系統(tǒng)或者框架的各種通知溢吻,因此也無(wú)可避免產(chǎn)生紛繁復(fù)雜的狀態(tài)维费。使用響應(yīng)式編程后,所有事件將成為異步的數(shù)據(jù)流促王,更加方便的是可以對(duì)這些數(shù)據(jù)流可以進(jìn)行組合變換掩完,最終我們只需要監(jiān)聽所關(guān)心的數(shù)據(jù)流的變化并做出響應(yīng)即可。
舉個(gè)有趣的例子來解釋一下:
當(dāng)你早上起床之后硼砰,你需要一邊洗漱一邊烤個(gè)面包且蓬,最后吃早飯。
傳統(tǒng)的編程方法:
func goodMorning() {
wake {
let group = DispatchGroup()
group.enter()
wash {
group.leave()
}
group.enter()
cook {
group.leave()
}
group.notify(queue: .main) {
eat {
print("Finished")
}
}
}
}
響應(yīng)式編程:
func reactiveGoodMorning() {
let routine = wake.rx.flapLatest {
return Observable.zip(wash, cook)
}.flapLatest {
return eat.rx
}
routine.subsribe(onCompleted: {
print("Finished")
})
}
不同于傳統(tǒng)可以看到 wake/wash/cook/eat 這幾個(gè)事件通過一些組合轉(zhuǎn)換被串聯(lián)成一個(gè)流题翰,我們也只需要訂閱這個(gè)流就可以在應(yīng)該響應(yīng)的時(shí)候得到通知恶阴。
Marble Diagram
為了更方便理解數(shù)據(jù)流诈胜,我們通常用一種叫 Marble Diagram 的圖象形式來表示數(shù)據(jù)流基于時(shí)間的變化。
我們用從左至右的箭頭表示時(shí)間線冯事,不同的形狀代表 Publisher 發(fā)出的值(Input)焦匈,豎線代表正常終止,叉代表發(fā)生錯(cuò)誤而終止昵仅。上圖中缓熟,上面一條時(shí)間線是一個(gè)數(shù)據(jù)流,中間的方塊代表組合變換摔笤,下方的另外一條時(shí)間線代表經(jīng)過變換后的數(shù)據(jù)流够滑。
Combine
現(xiàn)在可以進(jìn)入正題了,什么是 Combine吕世?
官方文檔的介紹是:
The Combine framework provides a declarative Swift API for processing values over time. These values can represent user interface events, network responses, scheduled events, and many other kinds of asynchronous data.
By adopting Combine, you’ll make your code easier to read and maintain, by centralizing your event-processing code and eliminating troublesome techniques like nested closures and convention-based callbacks.
簡(jiǎn)而言之彰触,Combine 可以使代碼更加簡(jiǎn)潔、易于維護(hù)命辖,也免除了飽受詬病的嵌套閉包和回調(diào)地獄况毅。Combine 是 Reactive Programming 在 Swift 中的一個(gè)實(shí)現(xiàn),更確切的說是對(duì) ReactiveX (Reactive Extensions, 簡(jiǎn)稱 Rx) 的實(shí)現(xiàn)尔艇,而這個(gè)實(shí)現(xiàn)正是基于觀察者模式的尔许。
Combine 是基于泛型實(shí)現(xiàn)的,是類型安全的终娃。它可以無(wú)縫地接入已有的工程母债,用來處理現(xiàn)有的 Target/Action
、Notification
尝抖、KVO
、callback/closure
以及各種異步網(wǎng)絡(luò)請(qǐng)求迅皇。
在 Combine 中昧辽,有幾個(gè)重要的組成部分:
- 發(fā)布者:Publiser
- 訂閱者:Subscriber
- 操作符:Operator
Publisher
在 Combine 中,Publisher 是觀察者模式中的 Observable登颓,并且可以通過組合變換(利用 Operator)重新生成新的 Publisher搅荞。
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure : Error
/// This function is called to attach the specified `Subscriber` to
/// this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
func receive<S>(subscriber: S) where S : Subscriber,
Self.Failure == S.Failure, Self.Output == S.Input
}
在 Publisher 的定義中,Output 代表數(shù)據(jù)流中輸出的值框咙,值的更新可能是同步咕痛,也可能是異步,F(xiàn)ailure 代表可能產(chǎn)生的錯(cuò)誤喇嘱,也就是說 Pubslier 最核心的是定義了值與可能的錯(cuò)誤茉贡。Publisher 通過 receive(subscriber:) 用來接受訂閱,并且要求 Subscriber 的值和錯(cuò)誤類型要一致來保證類型安全者铜。
例如來自官方的一個(gè)例子:
extension NotificationCenter {
struct Publisher: Combine.Publisher {
typealias Output = Notification
typealias Failure = Never
init(center: NotificationCenter, name: Notification.Name, object: Any? = nil)
}
}
這個(gè) Publisher 提供的值就是 Notification 類型腔丧,而且永遠(yuǎn)不會(huì)產(chǎn)生錯(cuò)誤(Never)放椰。這個(gè)擴(kuò)展非常有用,可以很方便地將任何 Notification 轉(zhuǎn)換成 Publisher愉粤,便于我們將應(yīng)用改造成 Reactive砾医。
讓我們?cè)倏匆粋€(gè)的例子:
let justPubliser = Publishers.Just("Hello")
justPubliser 會(huì)給每個(gè)訂閱者發(fā)送一個(gè) "Hello" 消息,然后立即結(jié)束(這個(gè)數(shù)據(jù)流只包含一個(gè)值)衣厘。
類似的如蚜,Combine 為我們提供了一些便捷的 Publisher 的實(shí)現(xiàn),除了上面的 Just影暴,這里也列出一些很有用的 Publisher错邦。
Empty
Empty 不提供任何值的更新,并且可以選擇立即正常結(jié)束坤检。
Once
Once 可以提供以下兩種數(shù)據(jù)流之一:
發(fā)送一次值更新兴猩,然后立即正常結(jié)束(和 Just 一樣)
立即因錯(cuò)誤而終止
Fail
Fail 和 Once 很像,也是提供兩種情況之一:
發(fā)送一次值更新早歇,然后立即因錯(cuò)誤而終止
立即因錯(cuò)誤而終止
Sequence
Sequence 將給定的一個(gè)序列按序通知到訂閱者倾芝。
Future
Future 初始化需要提供執(zhí)行具體操作的 closure,這個(gè)操作可以是異步的箭跳,并且最終返回一個(gè) Result晨另,所以 Future 要么發(fā)送一個(gè)值,然后正常結(jié)束谱姓,要么因錯(cuò)誤而終止借尿。在將一些異步操作轉(zhuǎn)換為 Publisher 時(shí)非常有用,尤其是網(wǎng)絡(luò)請(qǐng)求屉来。例如:
let apiRequest = Publishers.Future { promise in
URLSession.shared.dataTask(with: url) { data, _, _ in
promise(.success(data))
}.resume()
}
Deferred
Deferred 初始化需要提供一個(gè)生成 Publisher 的 closure路翻,只有在有 Subscriber 訂閱的時(shí)候才會(huì)生成指定的 Publisher,并且每個(gè) Subscriber 獲得的 Publisher 都是全新的茄靠。
那么茂契,在之前的 Just Publisher 例子中,假設(shè)我們要實(shí)現(xiàn)延遲兩秒后再發(fā)送消息呢慨绳?也很簡(jiǎn)單掉冶。我們前面說到 Publisher 都是可以組合變換的,這些組合變換可以通過操作符來實(shí)現(xiàn)的脐雪。
上面的例子利用 delay 操作符就可以改寫為:
let delayedJustPublisher = justPubliser.delay(for: 2, scheduler: DispatchQueue.main)
在文章的后半部分厌小,我們將了解一些常用操作符以及它們的使用場(chǎng)景。
Subscriber
和 Publisher 相對(duì)應(yīng)的战秋,Subscriber 就是觀察者模式中 Observer璧亚。
public protocol Subscriber : CustomCombineIdentifierConvertible {
/// The kind of values this subscriber receives.
associatedtype Input
/// The kind of errors this subscriber might receive.
///
/// Use `Never` if this `Subscriber` cannot receive errors.
associatedtype Failure : Error
/// Tells the subscriber that it has successfully subscribed to
/// the publisher and may request items.
///
/// Use the received `Subscription` to request items from the publisher.
/// - Parameter subscription: A subscription that represents the connection
/// between publisher and subscriber.
func receive(subscription: Subscription)
/// Tells the subscriber that the publisher has produced an element.
///
/// - Parameter input: The published element.
/// - Returns: A `Demand` instance indicating how many more elements
/// the subcriber expects to receive.
func receive(_ input: Self.Input) -> Subscribers.Demand
/// Tells the subscriber that the publisher has completed publishing,
/// either normally or with an error.
///
/// - Parameter completion: A `Completion` case indicating
/// whether publishing completed normally or with an error.
func receive(completion: Subscribers.Completion<Self.Failure>)
}
可以看出,從概念上和我們之前定義的簡(jiǎn)單觀察者模式相差無(wú)幾脂信。Publisher 在自身狀態(tài)改變時(shí)涨岁,調(diào)用 Subscriber 的三個(gè)不同方法(receive(subscription), receive(_:Input), receive(completion:))來通知 Subscriber拐袜。
這里也可以看出,Publisher 發(fā)出的通知有三種類型:
- Subscription:Subscriber 成功訂閱的消息梢薪,只會(huì)發(fā)送一次蹬铺,取消訂閱會(huì)調(diào)用它的 Cancel 方法來釋放資源
- Value(Subscriber 的 Input,Publisher 中的 Output):真正的數(shù)據(jù)秉撇,可能發(fā)送 0 次或多次
- Completion:數(shù)據(jù)流終止的消息甜攀,包含兩種類型:.finished 和 .failure(Error),最多發(fā)送一次琐馆,一旦發(fā)送了終止消息规阀,這個(gè)數(shù)據(jù)流就斷開了,當(dāng)然有的數(shù)據(jù)流可能永遠(yuǎn)沒有終止
大部分場(chǎng)景下我們主要關(guān)心的是后兩種消息瘦麸,即數(shù)據(jù)流的更新和終止谁撼。
Combine 內(nèi)置的 Subscriber 有三種:
- Sink
- Assign
- Subject
Sink 是非常通用的 Subscriber,我們可以自由的處理數(shù)據(jù)流的狀態(tài)滋饲。
例如:
let once: Publishers.Once<Int, Never> = Publishers.Once(100)
let observer: Subscribers.Sink<Publishers.Once<Int, Never>> = Subscribers.Sink(receiveCompletion: {
print("completed: \($0)")
}, receiveValue: {
print("received value: \($0)")
})
once.subscribe(observer)
// received value: 100
// completed: finished
Publisher 甚至提供了 sink(receiveCompletion:, receiveValue:) 方法來直接訂閱厉碟。
Assign 可以很方便地將接收到的值通過 KeyPath 設(shè)置到指定的 Class 上(不支持 Struct),很適合將已有的程序改造成 Reactive屠缭。
例如:
class Student {
let name: String
var score: Int
init(name: String, score: Int) {
self.name = name
self.score = score
}
}
let student = Student(name: "Jack", score: 90)
print(student.score)
let observer = Subscribers.Assign(object: student, keyPath: \Student.score)
let publisher = PassthroughSubject<Int, Never>()
publisher.subscribe(observer)
publisher.send(91)
print(student.score)
publisher.send(100)
print(student.score)
// 90
// 91
// 100
在例子中箍鼓,一旦 publisher 的值發(fā)生改變,相應(yīng)的呵曹,student 的 score 也會(huì)被更新款咖。
PassthroughSubject 這里是 Combine 內(nèi)置的一個(gè) Publisher,可能你會(huì)好奇:前面不是說 Subject 是一種 Observer 嗎奄喂,這里怎么又是 Pulisher 呢铐殃?
Subject
有些時(shí)候我們想隨時(shí)在 Publisher 插入值來通知訂閱者,在 Rx 中也提供了一個(gè) Subject 類型來實(shí)現(xiàn)跨新。Subject 通常是一個(gè)中間代理富腊,即可以作為 Publisher,也可以作為 Observer玻蝌。Subject 的定義如下:
public protocol Subject : AnyObject, Publisher {
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
func send(_ value: Self.Output)
/// Sends a completion signal to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates
/// whether publishing has finished normally or failed with an error.
func send(completion: Subscribers.Completion<Self.Failure>)
}
作為 Observer 的時(shí)候,可以通過 Publisher 的 subscribe(_:Subject) 方法訂閱某個(gè) Publisher词疼。
作為 Publisher 的時(shí)候俯树,可以主動(dòng)通過 Subject 的兩個(gè) send 方法,我們可以在數(shù)據(jù)流中隨時(shí)插入數(shù)據(jù)贰盗。目前在 Combine 中许饿,有三個(gè)已經(jīng)實(shí)現(xiàn)對(duì) Subject: AnySubject,CurrentValueSubject 和 PassthroughSubject 舵盈。
利用 Subject 可以很輕松的將現(xiàn)在已有代碼的一部分轉(zhuǎn)為 Reactive陋率,一個(gè)比較典型的使用場(chǎng)景是:
// Before
class ContentManager {
var content: [String] {
didSet {
delegate?.contentDidChange(content)
}
}
func getContent() {
content = ["hello", "world"]
}
}
// After
class RxContentController {
var content = CurrentValueSubject<[String], NSError>([])
func getContent() {
content.value = ["hello", "world"]
}
}
CurrentValueSubject 的功能很簡(jiǎn)單球化,就是包含一個(gè)初始值,并且會(huì)在每次值變化的時(shí)候發(fā)送一個(gè)消息瓦糟,這個(gè)值會(huì)被保存筒愚,可以很方便的用來替代 Property Observer。在上例中菩浙,以前需要實(shí)現(xiàn) delegate 來獲取變化巢掺,現(xiàn)在只需要訂閱 content 的變化即可,并且它作為一個(gè) Publisher劲蜻,可以很方便的利用操作符進(jìn)行組合變換陆淀。
PassthroughSubject 和 CurrentValueSubject 幾乎一樣,只是沒有初始值先嬉,也不會(huì)保存任何值轧苫。
AnyPublisher、AnySubscriber疫蔓、AnySubject
前面說到 Publisher含懊、Subscriber、Subject 是類型安全的鳄袍,那么在使用中不同類型的 Publisher绢要、Subscriber、Subject 勢(shì)必會(huì)造成一些麻煩拗小。
考慮下面這種情況:
class StudentManager {
let namesPublisher: ??? // what's the type?
func updateStudentsFromLocal() {
let student1 = Student(name: "Jack", score: 75)
let student2 = Student(name: "David", score: 80)
let student3 = Student(name: "Alice", score: 96)
let namesPublisher: Publishers.Sequence<[String], Never> = Publishers.Sequence<[Student], Never>(sequence: [student1, student2, student3]).map { $0.name }
self.namesPublisher = namesPublisher
}
func updateStudentsFromNetwork() {
let namesPublisher: Publishers.Future<[String], Never> = Publishers.Future { promise in
getStudentsFromNetwork {
let names: [String] = ....
promise(.success([names]))
}
}
self.namesPublisher = namesPublisher
}
}
這里 namesPublisher 究竟該是什么類型呢重罪?第一個(gè)方法返回的是 Publishers.Sequence<[String], Never>,第二個(gè)方法是 Publishers.Future<[String], Never>哀九,而我們是無(wú)法定義成 Publisher<[String], Never> 的剿配。
AnyPublisher、AnySubscriber阅束、AnySubject 正是為這樣的場(chǎng)景設(shè)計(jì)的呼胚,它們是通用類型,任意的 Publisher息裸、Subscriber蝇更、Subject 都可以通過 eraseToAnyPublisher()、eraseToAnySubscriber()呼盆、eraceToAnySubject() 轉(zhuǎn)化為對(duì)應(yīng)的通用類型年扩。
class StudentManager {
let namePublisher: AnyPublisher<[String, Never]>
func updateStudentsFromLocal() {
let namePublisher: AnyPublisher<[String, Never]> = Publishers.Sequence<[Student], Never>(sequence: students).map { $0.name }.eraseToAnyPublisher()
self.namePublisher = namePublisher
}
func updateStudentsFromNetwork() {
let namePublisher: AnyPublisher<[String, Never]> = Publishers.Future { promise in
promise(.success([names]))
}.eraseToAnyPublisher()
self.namePublisher = namePublisher
}
}
AnyPublisher、AnySubscriber访圃、AnySubject 的另外一個(gè)實(shí)用場(chǎng)景是創(chuàng)建自定義的 Publisher/Subscriber/Subject厨幻,因?yàn)樵诳蚣苤兴鼈円呀?jīng)是實(shí)現(xiàn)好相應(yīng)的協(xié)議了。
let justPubliser = AnyPublisher<String, NSError> { subscribe in
_ = subscribe.receive("hello") // ignore demand
subscribe.receive(completion: .finished)
}
let subscriber = AnySubscriber<String, NSError>(receiveValue: { input in
print("Received input: \(input)")
return .unlimited
}, receiveCompletion: { completion in
print("Completed with \(completion)")
})
justPubliser.subscribe(subscriber)
// Received input: hello
// Completed with finished
Cancellable
讓我們回到上文提到的 Future 的一個(gè)例子:
let apiRequest = Publishers.Future { promise in
URLSession.shared.dataTask(with: url) { data, _, _ in
promise(.success(data))
}.resume()
}
它似乎能夠很好的工作,但是一個(gè)比較經(jīng)典的場(chǎng)景是:用戶上傳某個(gè)文件况脆,上傳到一半發(fā)現(xiàn)選錯(cuò)了就點(diǎn)擊取消饭宾,我們也應(yīng)該取消這個(gè)上傳。類似的還有一些臨時(shí)生成的資源需要在取消訂閱時(shí)釋放格了。這在 Combine 中該怎么實(shí)現(xiàn)呢看铆?
Combine 中提供了 Cancellable 這個(gè)協(xié)議。定義很簡(jiǎn)單:
protocol Cancellable {
/// Cancel the activity.
func cancel()
}
前面我們提到 Publisher 在被訂閱時(shí)會(huì)給 Subscriber 發(fā)送一個(gè) Subscription 消息笆搓,這個(gè) Subscription 恰好也實(shí)現(xiàn)了 Cancellable 協(xié)議性湿,在取消訂閱時(shí),會(huì)調(diào)用它的 cancel 方法满败。
這樣肤频,我們就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的例子:
struct AnySubscription: Subscription {
private let cancellable: Cancellable
init(_ cancel: @escaping () -> Void) {
cancellable = AnyCancellable(cancel)
}
func cancel() {
cancellable.cancel()
}
}
let downloadPublisher = AnyPublisher<Data?, Never> { subscribe in
let task = URLSession.shared.uploadTask(with: request, fromFile: file) { (data, _, _) in
_ = subscribe.receive(data) // ignore demand
subscribe.receive(completion: .finished)
}
let subscription = AnySubscription {
task.cancel()
}
subscribe.receive(subscription: subscription)
task.resume()
}
let cancellable = downloadPublisher.sink { data in
print("Received data: \(data)")
}
// Cancel the task before it finishes
cancellable.cancel()
操作符
操作符是 Combine 中非常重要的一部分,通過各式各樣的操作符算墨,可以將原來各自不相關(guān)的邏輯變成一致的(unified)宵荒、聲明式的(declarative)的數(shù)據(jù)流。
轉(zhuǎn)換操作符:
- map/mapError
- flatMap
- replaceNil
- scan
- setFailureType
過濾操作符:
- filter
- compactMap
- removeDuplicates
- replaceEmpty/replaceError
reduce 操作符:
- collect
- ignoreOutput
- reduce
運(yùn)算操作符:
- count
- min/max
匹配操作符:
- contains
- allSatisfy
序列操作符:
- drop/dropFirst
- append/prepend
- prefix/first/last/output
組合操作符:
- combineLatest
- merge
- zip
錯(cuò)誤處理操作符:
- assertNoFailure
- catch
- retry
時(shí)間控制操作符:
measureTimeInterval
debounce
delay
throttle
timeout
其他操作符:
- encode/decode
- switchToLatest
- share
- breakpoint/breakpointOnError
- handleEvents
以下內(nèi)容僅供參考净嘀,目前并未完成所有的操作符的詳細(xì)介紹
map/mapError
map 將收到的值按照給定的 closure 轉(zhuǎn)換為其他值报咳,mapError 則將錯(cuò)誤轉(zhuǎn)換為另外一種錯(cuò)誤類型。
func map<T>(_ transform: (Output) -> T) -> Publishers.Just<T>