什么是Combine
“一套統(tǒng)一的聲明性API,用于處理隨時(shí)間變化的值,其有著支持泛型空执,類型安全,組成優(yōu)先穗椅,請(qǐng)求驅(qū)動(dòng)的特點(diǎn)”
這是 WWDC19 上蘋果推出 Combine 時(shí)的官方描述辨绊。在 iOS 的開(kāi)發(fā)者社區(qū)中基本都將其與 響應(yīng)式編程 掛鉤。如 OC 下的 ReactiveCocoa 與 Swift 下的 Rx 套件(RxSwift房待、RxCocoa等)邢羔,這些都是響應(yīng)式編程框架。
其他第三方響應(yīng)式編程框架不香嗎桑孩?開(kāi)發(fā)中引入第三方框架拜鹤,也就等于引入了一定風(fēng)險(xiǎn)(Bug、性能缺陷流椒、停止維護(hù)敏簿、甚至巨大的代碼量) 。Combine 的優(yōu)勢(shì)就是 “官方出品” ,意味著它能進(jìn)行系統(tǒng)底層優(yōu)化惯裕,更不用說(shuō)其與 Swift温数、UIKit、SwiftUI 等官方框架的深度融合蜻势。所以了解 Combine 是非常必要的撑刺。
Combine 的組成
Combine 的結(jié)構(gòu)跟其他響應(yīng)式框架類似,其中最基礎(chǔ)的組成分為三個(gè)部分握玛,簡(jiǎn)單說(shuō)明如下:
Publisher(發(fā)布者)
值類型够傍,描述了 值
和 錯(cuò)誤
是如何產(chǎn)生的,遵循 Publisher 協(xié)議
挠铲,協(xié)議中聲明了值類型與錯(cuò)誤類型(Output
與 Failure
)冕屯,聲明 Publisher 時(shí)需要指定這兩者。
// Publisher 協(xié)議主體
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
Subscriber(訂閱者)
引用類型拂苹,遵循 Subscriber 協(xié)議
安聘,根據(jù)其訂閱的 Publisher 配置有多種接收方法。
// Subscriber 協(xié)議主體
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public 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>)
}
訂閱者訂閱發(fā)布者后會(huì)返回一個(gè)遵循 Cancellable
協(xié)議的 AnyCancellable
瓢棒,作用上類似于其他響應(yīng)式框架中的 dispose浴韭。其控制著訂閱者的釋放,在開(kāi)發(fā)中音羞,可將其作為屬性持有囱桨,當(dāng)頁(yè)面銷毀時(shí),系統(tǒng)釋放 AnyCancellable 時(shí)嗅绰,其會(huì)自動(dòng)調(diào)用其內(nèi)部的 cancel() 方法進(jìn)行資源釋放舍肠。
Operator(操作符)
值類型。其本質(zhì)上也是 Publisher窘面,因此可被 Subscriber 訂閱翠语,其自身也能訂閱其他的 Publisher。Combine 中有不少操作符财边,常見(jiàn)于對(duì)發(fā)布者的數(shù)據(jù)進(jìn)行過(guò)濾修改等操作時(shí)使用肌括。將其看做是個(gè)“中間人”
,使用多個(gè) Operator 都是可以的酣难。
【可以通過(guò)一個(gè)例子來(lái)理解三者的關(guān)系:】
上面這個(gè)圖的例子
[發(fā)布者]
說(shuō)自己對(duì)錢沒(méi)有興趣谍夭,[操作符]
覺(jué)得他說(shuō)謊所以就將數(shù)據(jù)過(guò)濾掉并沒(méi)有繼續(xù)傳遞下去,而[訂閱者]
并不會(huì)知道發(fā)布者說(shuō)的話憨募。操作符也可以將發(fā)布者的這句話繼續(xù)傳遞下去讓訂閱者知道紧索,但老夫不愿意。[猛男微笑.gif]
一個(gè)雙向綁定的簡(jiǎn)單例子
Tips:示例基于 Xcode12 beta5
一個(gè)最簡(jiǎn)單的登錄界面菜谣,下面我們就實(shí)現(xiàn)一個(gè)開(kāi)發(fā)中最常見(jiàn)的雙向綁定珠漂,初始 ViewModel 如下:
struct LoginModel {
var account:String = ""
}
class LoginVM {
// 登錄狀態(tài)
enum LoginState {
case none
case success
case error
}
// model
var model = LoginModel()
// 登錄
func login(psw:String = "") {...}
}
將賬號(hào)輸入與模型綁定
在 WWDC19 時(shí)晚缩,蘋果整合了combine 與 Notifaction、URLSession媳危、Userdefault 三個(gè)系統(tǒng)組件荞彼。而在寫這個(gè)demo的時(shí),本想自定義個(gè) Publisher待笑,結(jié)果 Textfiled 竟也可以聯(lián)想出 Combine 相關(guān)方法鸣皂。本篇主要是介紹,老夫就偷個(gè)懶用系統(tǒng)的滋觉。
// 獲取發(fā)布者
let publisher = accountTF.publisher(for: \.text, options: NSKeyValueObservingOptions.new)
// 訂閱發(fā)布者
accountCancel = publisher.sink { [weak self](text) in
if let self = self {
// 將如數(shù)的賬號(hào)賦值給我們的 model
self.viewModel.model.account = text ?? ""
}
}
使用方法跟 RxSwift 等三方響應(yīng)式框架一樣签夭,并且更加的高效好用,僅僅兩句代碼椎侠。
第一句我們通過(guò) 賬號(hào)輸入組件獲取到發(fā)布者 publisher。其中的 \.text
是 Swift5.0(沒(méi)記錯(cuò)的話) 之后加入的特性措拇,相比 OC 中 KVC 使用字符串來(lái)指定關(guān)鍵字更加安全我纪,避免了輸入錯(cuò)誤引發(fā)問(wèn)題。
第二句就是創(chuàng)建訂閱者并讓其訂閱發(fā)布者丐吓,這里使用到了 sink
方法浅悉,其是 Publisher 協(xié)議
的擴(kuò)展方法: 將閉包綁定給訂閱者并訂閱發(fā)布者
,開(kāi)發(fā)者只需要通過(guò) sink
方法提供一個(gè)訂閱者回調(diào)的閉包給發(fā)布者即可實(shí)現(xiàn)訂閱券犁,意味著不用開(kāi)發(fā)者自己去實(shí)現(xiàn)訂閱者术健、再綁定發(fā)布者的操作。
注意 accountCancel
粘衬,其為 AnyCancelable
類型荞估,主要實(shí)現(xiàn)了 Cancellable 協(xié)議
,協(xié)議里只有一個(gè) cancel
方法稚新。只需要知道它由訂閱者實(shí)現(xiàn)勘伺,在其自身被釋放時(shí)調(diào)用 cancel 來(lái)釋放資源,這么一看跟 RxSwift 中的 DisposeBag 類似褂删。其與訂閱者生命周期相關(guān)飞醉,持有它,訂閱就會(huì)一直生效屯阀。
將請(qǐng)求結(jié)果跟視圖綁定
上邊我們用了系統(tǒng)生成的 publisher缅帘,那關(guān)于 model 層有沒(méi)有啥系統(tǒng)提供的東西呢?還真有难衰。
@Published var loginState:LoginState = .none
@Published
是系統(tǒng)提供給我們用來(lái)修飾屬性的钦无,其只能在 Class 中使用。從其寫法能看出它其實(shí)是一個(gè)屬性包裝器(PropertyWrapper):
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct Published<Value> {
public init(wrappedValue: Value)
public init(initialValue: Value)
// 發(fā)布者定義
public struct Publisher : Publisher {
public typealias Output = Value
public typealias Failure = Never
public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
}
// 發(fā)布者實(shí)例
public var projectedValue: Published<Value>.Publisher { mutating get set }
}
由上可知召衔,@Published
的屬性包裝器里讓屬性持有了個(gè)自己聲明的發(fā)布者铃诬。這樣就可以讓被@Published
標(biāo)記的屬性自動(dòng)生成發(fā)布者。
訂閱也很簡(jiǎn)單:
loginStateCancel = viewModel.$loginState.sink { (state) in
// 各種操作
}
使用 sink 方法訂閱 viewModel.$loginState
,這個(gè) loginState 不是個(gè)枚舉么...關(guān)鍵在 $
符號(hào)上...這里的viewModel.$loginState
實(shí)際上返回的是:
Published<LoginVM.LoginState>.Publisher
一個(gè)發(fā)布者趣席。通過(guò)$
符號(hào)訪問(wèn)屬性是獲取屬性包裝器中的自定義屬性 projectedValue 的值兵志,在 @Published 中,這個(gè)自定義屬性就是系統(tǒng)生成的發(fā)布者宣肚。關(guān)于屬性包裝器可以看看Property Wrappers
這里延伸出了一個(gè)問(wèn)題: 每個(gè)需要綁定/觀察的鍵都被 @Published 標(biāo)記想罕,然后又訂閱,可當(dāng)面對(duì)的是一個(gè)復(fù)雜的模型時(shí)就會(huì)產(chǎn)生大量重復(fù)操作霉涨。有沒(méi)有...
當(dāng)然有按价!使用 ObservableObject
協(xié)議:
class LoginVM: ObservableObject {...}
ObservableObject 協(xié)議中定義了一個(gè)發(fā)布器,并在協(xié)議的擴(kuò)展中實(shí)現(xiàn)了默認(rèn)的發(fā)布器笙瑟,這樣就讓遵循協(xié)議的類默認(rèn)擁有了一個(gè)發(fā)布器楼镐,獲取回調(diào)發(fā)布器的屬性為objectWillChange
。
Tips:被 @Published 標(biāo)記的屬性更新前會(huì)回調(diào)往枷,未被標(biāo)記的屬性則不會(huì)框产。
了解了這些,就可以通過(guò) viewModel 的 objectWillChange 獲取到發(fā)布者并訂閱來(lái)監(jiān)聽(tīng)所有被 @Published 標(biāo)記的屬性更改的回調(diào)
loginStateCancel = viewModel.objectWillChange.sink { [weak self]() in
print("登錄狀態(tài)即將發(fā)生改變:\(self?.viewModel.loginState)")
}
細(xì)心的你肯定發(fā)現(xiàn) objectWillChange 返回的發(fā)布者错洁,會(huì)在操作前回調(diào)秉宿,此時(shí)去獲取屬性還是舊值
,查看協(xié)議后發(fā)現(xiàn)目前只有這么一個(gè)發(fā)布器屯碴,未來(lái)會(huì)不會(huì)推出objectDidChange
不得而知描睦,我們是等蘋果還是自己動(dòng)手實(shí)現(xiàn)一個(gè)更新后的發(fā)布者,甚至粗暴的加個(gè)異步延時(shí)呢导而?挖了個(gè)坑忱叭。
總結(jié)
總的來(lái)說(shuō),可將 Combine 看作一種觀察者模式嗡载,其分為 [發(fā)布者] 和 [訂閱者] 窑多,兩者配合處理隨著時(shí)間變化的值,還有操作符用來(lái)修改發(fā)布者的值洼滚。
Combine 在 WWDC19 上推出埂息,而 WWDC20 上沒(méi)有什么大的變動(dòng),倒是默默的推出了更多融合到系統(tǒng)架構(gòu)中的功能遥巴。說(shuō)明 Combine 的架構(gòu)基本確定千康,未來(lái)也不會(huì)再有什么傷筋動(dòng)骨的變動(dòng),以后只會(huì)新增更多的支持特性铲掐。
如果開(kāi)發(fā)者的應(yīng)用從 iOS13 為最低版本開(kāi)發(fā)新應(yīng)用的話拾弃,推薦使用 Combine 替代 ReactiveCocoa,RxSwift 等三方框架摆霉。
下一篇豪椿,深入一步奔坟,看看在 Combine 中如何解鎖自定義的姿勢(shì)。