譯自 RxSwift to Combine: The Complete Transition Guide
不逐字翻譯了,只翻主要的知識(shí)點(diǎn)信息。
簡(jiǎn)介
Combine 是swift新推出的一種面向 響應(yīng)式編程的框架。
很多開發(fā)者都想從RxSwift 切換到Combine,兩者之間有很多的相似之處。
但細(xì)節(jié)方面還是有很多區(qū)別的,如果你想切換過來,這篇文章會(huì)列出 RxSwift和Combine之間的 操作符,函數(shù),類型等等的映射。
響應(yīng)式編程可以讓 數(shù)據(jù)狀態(tài)在對(duì)象之間,工程之間,甚至app之間互相同步。
在ios開發(fā)中,最常見的就是在UI和Model之間進(jìn)行狀態(tài)同步。在之前很長(zhǎng)一段時(shí)間里,RxSwift都是最好的選擇。但apple推出了 跟SwiftUI 更契合的Combine 作為響應(yīng)式編程的框架,基本可以完全替代RxSwift的所有功能了。
此文章將分為 Combine是怎么工作的 切換到Combine是好的選擇嗎 怎么更簡(jiǎn)單地從RxSwift切換到Combine 三個(gè)部分。
RxSwift 和 Combine的主要區(qū)別
| RxSwift | Combine | |
|---|---|---|
| 支持的iOS版本 | iOS 8.0+ | iOS 13.0+ |
| 支持的平臺(tái) | iOS, macOS, tvOS, watchOS, Linux | iOS, macOS, tvOS, watchOS, UIKit for Mac |
| 框架所屬 | 第三方 | Apple第一方,SDK內(nèi)置 |
| 誰來維護(hù)? | 開源社區(qū) | Apple技術(shù)團(tuán)隊(duì) |
| 協(xié)作的UI框架 | RxCocoa | SwiftUI |
Combine是怎么工作的
考慮到本文章是介紹如何從RxSwift遷移到Combine, 假設(shè)讀者已經(jīng)對(duì)RxSwift有一定的了解了。 如果需要了解RxSwift,可以參考 RxSwift repository on GitHub.
Combine在很多方面跟RxSwift都有相似之處以及對(duì)等的映射概念映射,比如 方法,類型聲明等。
如果要找出兩者之間更深層次的差異,需要對(duì)Combine進(jìn)行更深地挖掘,看看它的底層機(jī)制。
Publisher
跟RxSwift的Observable 相對(duì)應(yīng)的是Combine里的Publisher。
在RxSwift里是一個(gè)類,而在Combine里是一個(gè)協(xié)議。
protocol Publisher {
associatedtype Output
associatedtype Failure: Error
func receive<S: Subscriber>(subscriber: S)
where Self.Failure == S.Failure,
Self.Output == S.Input
}
Combine 不會(huì)指定 Wrapper types來描述它的唯一特征,比如RxSwift里的Infallible, Maybe or Single。
但每個(gè)Publisher也有自己的自定義類型,借由該類型,Publisher的特征也能被推導(dǎo)出來。
RxSwift的Observable 需要實(shí)現(xiàn)subscribe 函數(shù),對(duì)應(yīng)的Publisher需要實(shí)現(xiàn) receive函數(shù),它們的功能基本一致。相比于RxSwift而言,Publisher指定了一個(gè) Failure類型,可以表明該publisher 是否/如何 失敗。對(duì)于那些不用處理失敗的Publisher,我們?cè)O(shè)定Failure類型為Never 即可。
Combine的Publisher 可以是 值類型(比如struct)或是 引用類型(比如類)。大多數(shù)的Publisher都是值類型的。
在RxSwift里,操作符一般就是簡(jiǎn)單地返回一個(gè)Observable 類型,而Combine里返回的是一個(gè)混合類型,比較復(fù)雜;比如Timer.TimerPublisher, CombineLatest<Just<Int>, Just<Int>>. 鑒于自己定義一個(gè)混合類型比較麻煩,我們可以就用 AnyPublisher 來簡(jiǎn)化Publisher的使用。
舉個(gè)例子:AnyPublisher<Int, Never>,可以作為一個(gè) 不會(huì)失敗的拋出Int值的Publisher。

Subscriber
Subscriber (RxSwift: Observer)可以接收訂閱信息(subscriptions), 輸入數(shù)據(jù)(inputs),以及結(jié)束回調(diào)(completions). 這點(diǎn)跟RxSwift不同,沒有聲明包含了數(shù)據(jù)回調(diào)以及 結(jié)束回調(diào)的event的枚舉,但提供了單獨(dú)的方法來分別處理對(duì)應(yīng)的事件。
protocol Subscriber: CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
在上面的代碼片段里,你可以看到Subscriber 也聲明了一個(gè)Failure 類型,決定了它能失敗,以及失敗的相關(guān)類型。對(duì)應(yīng)的RxSwift也聲明了獨(dú)立的Error事件。Combine非常明顯的表明了 失敗會(huì)導(dǎo)致Publisher 終結(jié)(RxSwift里,error事件也表明了 Observable的終結(jié))。
Subscribers 必須是類,因?yàn)?subscription要引用它來做數(shù)據(jù)傳遞,而不能在訂閱期間重新創(chuàng)建多次Subscribers(值類型比如struct 會(huì)在數(shù)據(jù)變化時(shí)會(huì)重新創(chuàng)建實(shí)例)。
Subscription
每當(dāng)你訂閱時(shí),就會(huì)創(chuàng)建一個(gè) Subscription,它會(huì)持有所有必要的資源的引用,而且可以隨時(shí)cancel(對(duì)應(yīng)RxSwift里的DisposeBag)。這也是為什么Subscription 必須是類。
protocol Subscription: Cancellable, ... {
func request(_ demand: Subscribers.Demand)
}
protocol Cancellable {
func cancel()
}
與RxSwift相比,Combine允許訂閱者隨時(shí)可以去獲取更多數(shù)據(jù),并以 backpressure support 的形式返回;因此 實(shí)際上是訂閱者在主動(dòng)請(qǐng)求數(shù)據(jù)而不是 Publisher來決定什么時(shí)候?qū)?shù)據(jù)傳出去。
這能讓訂閱者不會(huì)被大量數(shù)據(jù)堵塞而導(dǎo)致無法及時(shí)處理,這看上去更像是可控制的數(shù)據(jù)流。
如果想更深入地了解 backpressure support ,可以參考 Processing Published Elements with Subscribers
Subject
Subject 跟RxSwift的差不多,可以看作是 Subscriber 和 Publisher 的組合,既可以發(fā)送數(shù)據(jù)也能接收數(shù)據(jù)。
protocol Subject: AnyObject, Publisher {
func send(_ value: Self.Output)
func send(completion: Subscribers.Completion<Self.Failure>)
func send(subscription: Subscription)
}
一方面你可以將數(shù)據(jù)傳給 Subject,另一方面由于 Subject 遵從了 Publisher協(xié)議,它也可以被監(jiān)聽來獲取數(shù)據(jù)。
在Combine里,訂閱者 receive 而 Subject send 事件。盡管如此,在每個(gè)subject里,每當(dāng)value 遵從 Subscriber協(xié)議時(shí),你都必須創(chuàng)建一個(gè) AnySubscriber。
綜上所述,RxSwift和Combine 大部分功能都是一樣的,但在接口方面有一些很小的區(qū)別。 那么哪個(gè)更適合我們呢?
切換到Combine是好的選擇嗎
最大的爭(zhēng)議點(diǎn)在于,Combine是apple的第一方框架,使用它你就不用做一些多余的framework 依賴配置工作,app也體積也會(huì)小一些。
當(dāng)然,RxSwift也沒有明確表明是否會(huì)有錯(cuò)誤發(fā)生。一般情況下,直接就用 Observable 然后等著數(shù)據(jù)過來就行了。 但在Combine里,就明確表明了Publisher 可能會(huì)失敗,而且失敗后產(chǎn)生的錯(cuò)誤是啥。
但這些可失敗和不會(huì)失敗的publisher的交互可能會(huì)非常復(fù)雜,你得在每個(gè)使用處都加上 eraseToAnyPublisher(),但幫助也不大。
在 backpressure support 的幫助下,Combine 能考慮得更加周到。訂閱者可以隨時(shí)獲取數(shù)據(jù),而不用等待Publisher 來發(fā)送數(shù)據(jù)。這更符合我們的直覺,特別是在一些很耗時(shí)的操作發(fā)生時(shí),大量的數(shù)據(jù)同時(shí)在publisher 進(jìn)出。
簡(jiǎn)單來說,Combine 更適應(yīng)整個(gè)swift的環(huán)境。很多擴(kuò)展方法已經(jīng)在常用的類型里實(shí)現(xiàn)了(比如 Timer.publisher, (0...3).publisher, Optional<String>.none.publisher),這些方法讓Combine的功能更加的全面。
不過Combine不像RxSwift那樣全面和強(qiáng)大,是么?
RxSwift 沒有像Combine 那么多的編譯問題。RxSwift不用管 失敗的錯(cuò)誤類型,而且單個(gè)泛型類型的檢查比兩個(gè)要簡(jiǎn)單得多。
RxSwift支持iOS9以及以上版本,但Combine只能用在iOS13 以及以上版本。它倆在所有的apple 平臺(tái)上都能work,但Combine對(duì)于linux的支持還是有缺失。OpenCombine 可以幫助解決這個(gè)問題,它用的是跟Combine一樣的接口,且能支持更多平臺(tái)。
Combine的命名規(guī)則更契合iOS平臺(tái),而RxSwift 更契合跨平臺(tái)模式的規(guī)則。
RxSwift的技術(shù)社區(qū)已經(jīng)存在很長(zhǎng)一段時(shí)間了,在維護(hù)和擴(kuò)展框架方面已經(jīng)做了很多改進(jìn)了。社區(qū)成員們?yōu)镽xSwift 擴(kuò)展了很多關(guān)于特定場(chǎng)景的功能,包括很多自定義的操作符和類型等。而Combine是一個(gè)封閉的框架,而且只推出了三年而已,只在少部分的項(xiàng)目里被使用到,如果你需要自己寫一些字定義的操作符,還不如直接使用現(xiàn)有的知識(shí)點(diǎn)來實(shí)現(xiàn)。
現(xiàn)在你要寫一個(gè)字定義的Publisher比自定義的Observable要麻煩許多。在RxSwift里,你只要用 Observable.create 來發(fā)送數(shù)據(jù),發(fā)送錯(cuò)誤,以及完成事件即可。在Combine里卻沒有對(duì)應(yīng)的操作,你可以用Future (基本上就是一個(gè)帶completion回調(diào)的Publisher,且completion只會(huì)調(diào)用一次) 或是寫一個(gè)自定義的Publisher 來模擬 Observable.create 的行為(我們接下來就會(huì)講到這點(diǎn))
總結(jié)一下
回到我們之前的 該用RxSwift還是Combine的問題,這個(gè)要看情況的。
如果你想要干掉之前那些“多余的”依賴關(guān)系,而且你的app支持iOS13 以及以上版本,且不用支持iOS 以外的平臺(tái),那Combine就是比較好的選擇了。
怎么更簡(jiǎn)單地從RxSwift切換到Combine
我們搜集了下面的切換引導(dǎo)信息,你可以直接搜索RxSwift的方法,操作符來找到Combine里的對(duì)應(yīng)替換方案。
一些簡(jiǎn)單的說明
一般來說,Combine在處理錯(cuò)誤時(shí),相關(guān)接口看上去比較復(fù)雜。給個(gè)小提示, setFailureType(to: Failure.self) 更容易理解,而不是當(dāng)作 mapError { _ -> Failure in } 來看待。
下面的這些鏈接在我們從RxSwift切換到Combine時(shí)提供了很大的幫助。
- CombineExt 里是Combine技術(shù)社區(qū)提供的一些 當(dāng)前系統(tǒng)Combine里沒有支持的操作符。
- cheat sheet 也提供了不少幫助。
類型
Disposable
| RxSwift | Combine |
|---|---|
| Disposable | Cancellable Use AnyCancellable to create them like RxSwift’s Disposables.create |
| DisposeBag | Set<AnyCancellable> any RandomAccessCollection of AnyCancellable of course, you can also create references one by one |
?? 注意,subscriptions 會(huì)在 Cancellable 析構(gòu)后直接cancel掉。
Publishers
| RxSwift | Combine |
|---|---|
| Observable<Element> |
AnyPublisher<Element, Error>或者 其他的 Publisher 類型 同樣根據(jù)拋出的錯(cuò)誤類型,Combine也能創(chuàng)建對(duì)應(yīng)的Failure類型 |
| Single<Element> |
AnyPublisher<Element, Error> Future<Element, Error> 可惜不能保證只拋出一個(gè)值,除非使用Future publisher |
| ConnectableObservable<Element> | ConnectablePublisher |
| Infallible<Element> |
AnyPublisher<Element, Never> 注意:Failure在這里的類型是 Never |
| Maybe<Element> | 在Combine里沒有對(duì)應(yīng)的東東 |
Subjects
| RxSwift | Combine |
|---|---|
| BehaviorSubject | CurrentValueSubject |
| PublishSubject | PassthroughSubject |
| ReplaySubject | Combine里沒有 ?? CombineExt里的替代方案 |
| AsyncSubject | Combine 里沒有 |
Relays
Relay是Subject的一個(gè)變種,他跟Subject的不同之處在于 無法發(fā)送/接收 完成事件。
class Relay<SubjectType: Subject>: Publisher,
CustomCombineIdentifierConvertible where SubjectType.Failure == Never {
typealias Output = SubjectType.Output
typealias Failure = SubjectType.Failure
let subject: SubjectType
init(subject: SubjectType) {
self.subject = subject
}
func send(_ value: Output) {
subject
.send(value)
}
func receive<S: Subscriber>(subscriber: S)
where Failure == S.Failure, Output == S.Input {
subject
.subscribe(on: DispatchQueue.main)
.receive(subscriber: subscriber)
}
}
typealias CurrentValueRelay<Output> = Relay<CurrentValueSubject<Output, Never>>
typealias PassthroughRelay<Output> = Relay<PassthroughSubject<Output, Never>>
extension Relay {
convenience init<O>(_ value: O)
where SubjectType == CurrentValueSubject<O, Never> {
self.init(subject: CurrentValueSubject(value))
}
convenience init<O>()
where SubjectType == PassthroughSubject<O, Never> {
self.init(subject: PassthroughSubject())
}
}
Observer Operators
| RxSwift | Combine |
|---|---|
| asObserver() | AnySubscriber |
| on(_:) |
Subscriber ? receive(_:) ? receive(completion:) Subject ? send(_:) ? send(completion:) |
| onNext(_:) |
Subscriber ? receive(_:) Subject ? send(_:) |
| onError(_:) |
Subscriber ?receive(completion: .failure(<error>)) Subject ? send(completion: .failure(<error>)) |
| onCompleted() |
Subscriber ? receive(completion: .finished) Subject ? send(completion: .finished) |
| mapObserver(_:) | Combine里沒有對(duì)應(yīng)的,不過實(shí)現(xiàn)起來很簡(jiǎn)單 ??replacement in CombineExt |
如果要修改subscriber的 Input或 Failure 類型,可以使用下面這個(gè)擴(kuò)展方式
extension Subscriber {
func map<Input>(
_ map: @escaping (Input) -> Self.Input
) -> AnySubscriber<Input, Failure> {
.init(
receiveSubscription: receive,
receiveValue: { self.receive(map($0)) },
receiveCompletion: receive
)
}
func mapError<Failure>(
_ map: @escaping (Failure) -> Self.Failure
) -> AnySubscriber<Input, Failure> {
.init(
receiveSubscription: receive,
receiveValue: receive,
receiveCompletion: { completion in
switch completion {
case let .failure(error):
self.receive(completion: .failure(map(error)))
case .finished:
self.receive(completion: .finished)
}
}
)
}
}
Scheduling
RxSwift 有很多不同的Scheduler 類型,比如:MainScheduler,ConcurrentDispatchQueueScheduler, SerialDispatchQueueScheduler, CurrentThreadScheduler, HistoricalScheduler, OperationQueueScheduler,以及其他更多類型。
而在Combine里,這個(gè)就很簡(jiǎn)單了??梢灾苯邮褂肎CD(Grand Central Dispatc)的相關(guān)類型就可以了。比如 DispatchQueue, OperationQueue 以及 RunLoop。
Combine里唯一的Scheduler類型只有 ImmediateScheduler,它能讓任務(wù)直接同步執(zhí)行(跟CurrentThreadScheduler 差不多)。
在這篇文章里可以獲取更多關(guān)于Combine里的scheduling 的信息。
Factory Functions
Publisher / Observable Operators
| RxSwift | Combine |
|---|---|
| amb(_:) | Combine里沒有 ?? replacement in CombineExt |
| asCompletable() | Combine里沒有 |
| asObservable() | eraseToAnyPublisher() |
| buffer( timeSpan: count: scheduler: ) |
collect( .byTimeOrCount(::_:), options: ) |
| catch(:) catchError(:) |
catch(_:) tryCatch(_:) |
| catchAndReturn(:) catchErrorJustReturn(:) |
replaceError(with:) |
| compactMap(_:) |
compactMap(_:) tryCompactMap(_:) |
| concat(...) |
append Publishers.Concatenate |
| concatMap(_:) | Combine里沒有 可以用 reduce(::)和 append(_:)來實(shí)現(xiàn) |
| debounce(_:scheduler:) | debounce( for: scheduler: options: ) |
| debug( _: trimOutput: file: line: function: ) |
print(_:to:) |
| delay(_:scheduler:) | delay( for: tolerance: scheduler: options: ) |
| delaySubscription( _: scheduler: ) |
Combine里沒有 可以用 Deferred和 delay(for:scheduler:options:)的組合來替代 |
| dematerialize() | Combine里沒有 ?? replacement in CombineExt |
| distinctUntilChanged(...) |
removeDuplicates() 當(dāng)Output是Equatable時(shí)用 removeDuplicates(by:) tryRemoveDuplicates(by:) |
| do( onNext: afterNext: onError: afterError: onCompleted: afterCompleted: onSubscribe: onSubscribed: onDispose: ) |
handleEvents( receiveSubscription: receiveOutput: receiveCompletion: receiveCancel: receiveRequest: ) 沒有onDispose: 在publisher complete時(shí),cancel不會(huì)被調(diào)用。 |
| element(at:) | output(at:) |
| enumerated() | Combine里沒有 建議可以用 scan(::)魔改一下 |
| filter(_:) |
filter(_:) tryFilter(_:) |
| first() | first() |
| flatMapFirst(_:) | Combine里沒有 |
| flatMap(_:) |
flatMap(_:) tryFlatMap(_:) |
| flatMapLatest(_:) |
map(_:)接上 switchToLatest() ?? replacement in CombineExt |
| groupBy(keySelector:) | 沒有 |
| ifEmpty(default:) | replaceEmpty(with:) |
| ifEmpty(switchTo:) | 沒有 |
| ignoreElements() | ignoreOutput() |
| map(_:) |
map(_:) tryMap(_:) |
| materialize() | 沒有 ?? replacement in CombineExt |
| multicast(_:) | multicast(subject:) |
| multicast(makeSubject:) | multicast(_:) |
| observe(on:) |
observe(on:) Combine用的是不同的scheduler類型! |
| publish() |
multicast { PassthroughSubject() } makeConnectable() |
| reduce(into:_:) | 沒有 不過可以用 reduce 來實(shí)現(xiàn) |
| reduce(::) |
reduce(::) tryReduce(::) |
| reduce( _: accumulator: mapResult: ) |
reduce(::)接上 map(_:) 或 tryMap(_:) |
| refCount() | autoconnect() |
| replay(_:) | 沒有 可以用 multicast(_:)和 ReplaySubject 來實(shí)現(xiàn) |
| replayAll() | 沒有 |
| retry() |
retry(.max) 可能有微小的意義上的差別,但實(shí)際使用中一般沒區(qū)別 |
| retry(_:) | retry(_:) |
| retry(when:) | 沒有 |
| sample(_:defaultValue:) | 沒有 |
| scan(into:_:) | 沒有 可以用一般的 scan(::)來實(shí)現(xiàn) |
| scan(::) |
scan(::) tryScan(::) |
| share(replay:scope:) |
share() 或者用 multicast(_:)加上 PassthroughSubject和 autoconnect() 或者用 multicast(_:)加上 ReplaySubject ?? extension in CombineExt |
| single() | 沒有 當(dāng)有且只有一個(gè)值時(shí),用 first(),否則會(huì)報(bào)錯(cuò) |
| single(_:) | 沒有 可以用 filter(_:)接上 single()的替代方案 |
| skip(_:) | dropFirst(_:) |
| skip(while:) | drop(while:) |
| skip(until:) | drop(untilOutputFrom:) |
| startWith(...) | prepend(...) |
| subscribe(_:) | 最類似的接口: sink( receiveValue: receiveCompletion: ) |
| subscribe(on:) |
subscribe(on:options:) Combine用的是不同的scheduler類型! |
| subscribe( onNext: onError: onCompleted: onDisposed: ) |
最類似的接口: sink( receiveValue: receiveCompletion: ) |
| subscribe( with: onNext: onError: onCompleted: onDisposed: ) |
最類似的接口: sink( receiveValue: receiveCompletion: ) 或 [weak object]
|
| switchLatest() | switchToLatest() |
| take(_:) | prefix(_:) |
| take(for:scheduler:) | 沒有 可以用以下方式解決: prefix(untilOutputFrom: Timer.publish( every: <time>, on: <scheduler> ) .autoconnect() .prefix(1) ) ?? replacement in CombineExt |
| take(until:) | prefix(untilOutputFrom:) |
| take(until:behavior:) | 用 prefix(while:)的相反條件來替代,但沒有 behavior參數(shù)功能了 |
| take(while:behavior:) | prefix(while:),但沒有 behavior參數(shù)功能了 |
| takeLast(_:) | 沒有 最接近的方案: reduce([]) { .flatMap { $0.suffix(<count>).publisher } |
| throttle( _: latest: scheduler: ) |
throttle( for: scheduler: latest: ) |
| timeout( _: other: scheduler: ) |
最接近的方案: timeout( _: scheduler: options: customError: ) 然后再 catch(_:), 用map轉(zhuǎn)到其他publisher上 |
| timeout(_:scheduler:) | timeout( _: scheduler: options: customError: ) |
| toArray() | collect() |
| window( timeSpan: count: scheduler: ) |
collect( .byTime(<scheduler>, <time>) ) collect( .byTimeOrCount( <scheduler>, <time>, <count> ) ) |
| withLatestFrom(_:) | 沒有 ?? replacement in CombineExt |
| withLatestFrom( _: resultSelector: ) |
沒有 ?? replacement in CombineExt |
| withUnretained(_:) | 最接近的方案: compactMap { [weak object] value in object.map { ($0, value) } } |
| withUnretained( _: resultSelector: ) |
最接近的方案: compactMap { [weak object] value in object.map { resultSelector($0, value) } } |
總結(jié)一下
對(duì)我們來說,總是要調(diào)用 setFailureType(to:) 和 eraseToAnyPublisher() 感覺不舒坦,除此之外,我們還是覺得Combine 很棒的。