iOS Combine - 1.初见 Combine

什么是Combine

“一套统一的声明性API,用于处理随时间变化的值,其有着支持泛型,类型安全,组成优先,请求驱动的特点”

这是 WWDC19 上苹果推出 Combine 时的官方描述。在 iOS 的开发者社区中基本都将其与 响应式编程 挂钩。如 OC 下的 ReactiveCocoa 与 Swift 下的 Rx 套件(RxSwift、RxCocoa等),这些都是响应式编程框架。

其他第三方响应式编程框架不香吗?开发中引入第三方框架,也就等于引入了一定风险(Bug、性能缺陷、停止维护、甚至巨大的代码量) 。Combine 的优势就是 “官方出品” ,意味着它能进行系统底层优化,更不用说其与 Swift、UIKit、SwiftUI 等官方框架的深度融合。所以了解 Combine 是非常必要的。


Combine 的组成

Combine 的结构跟其他响应式框架类似,其中最基础的组成分为三个部分,简单说明如下:

Publisher(发布者)

值类型,描述了 错误 是如何产生的,遵循 Publisher 协议,协议中声明了值类型与错误类型(OutputFailure),声明 Publisher 时需要指定这两者。

// Publisher 协议主体
@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 协议,根据其订阅的 Publisher 配置有多种接收方法。

// Subscriber 协议主体
@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>)
}

订阅者订阅发布者后会返回一个遵循 Cancellable协议的 AnyCancellable,作用上类似于其他响应式框架中的 dispose。其控制着订阅者的释放,在开发中,可将其作为属性持有,当页面销毁时,系统释放 AnyCancellable 时,其会自动调用其内部的 cancel() 方法进行资源释放。

Operator(操作符)

值类型。其本质上也是 Publisher,因此可被 Subscriber 订阅,其自身也能订阅其他的 Publisher。Combine 中有不少操作符,常见于对发布者的数据进行过滤修改等操作时使用。将其看做是个“中间人”,使用多个 Operator 都是可以的。

【可以通过一个例子来理解三者的关系:】

关系举例

上面这个图的例子[发布者]说自己对钱没有兴趣,[操作符]觉得他说谎所以就将数据过滤掉并没有继续传递下去,而[订阅者]并不会知道发布者说的话。操作符也可以将发布者的这句话继续传递下去让订阅者知道,但老夫不愿意。[猛男微笑.gif]


一个双向绑定的简单例子

Tips:示例基于 Xcode12 beta5

一个最简单的登录界面,下面我们就实现一个开发中最常见的双向绑定,初始 ViewModel 如下:

struct LoginModel {
    var account:String = ""
}

class LoginVM {
    
    // 登录状态
    enum LoginState {
        case none            
        case success
        case error
    }
    
    // model
    var model = LoginModel()
    
    // 登录
    func login(psw:String = "") {...}
}

将账号输入与模型绑定

在 WWDC19 时,苹果整合了combine 与 Notifaction、URLSession、Userdefault 三个系统组件。而在写这个demo的时,本想自定义个 Publisher,结果 Textfiled 竟也可以联想出 Combine 相关方法。本篇主要是介绍,老夫就偷个懒用系统的。

// 获取发布者
let publisher = accountTF.publisher(for: \.text, options: NSKeyValueObservingOptions.new)

// 订阅发布者
accountCancel = publisher.sink { [weak self](text) in
    if let self = self {
        // 将如数的账号赋值给我们的 model
        self.viewModel.model.account = text ?? ""
    }
}

使用方法跟 RxSwift 等三方响应式框架一样,并且更加的高效好用,仅仅两句代码。

第一句我们通过 账号输入组件获取到发布者 publisher。其中的 \.text 是 Swift5.0(没记错的话) 之后加入的特性,相比 OC 中 KVC 使用字符串来指定关键字更加安全,避免了输入错误引发问题。

第二句就是创建订阅者并让其订阅发布者,这里使用到了 sink 方法,其是 Publisher 协议的扩展方法: 将闭包绑定给订阅者并订阅发布者 ,开发者只需要通过 sink 方法提供一个订阅者回调的闭包给发布者即可实现订阅,意味着不用开发者自己去实现订阅者、再绑定发布者的操作。

注意 accountCancel,其为 AnyCancelable 类型,主要实现了 Cancellable 协议,协议里只有一个 cancel 方法。只需要知道它由订阅者实现,在其自身被释放时调用 cancel 来释放资源,这么一看跟 RxSwift 中的 DisposeBag 类似。其与订阅者生命周期相关,持有它,订阅就会一直生效。

将请求结果跟视图绑定

上边我们用了系统生成的 publisher,那关于 model 层有没有啥系统提供的东西呢?还真有。

@Published var loginState:LoginState = .none

@Published 是系统提供给我们用来修饰属性的,其只能在 Class 中使用。从其写法能看出它其实是一个属性包装器(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)

    // 发布者定义
    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
    }

    // 发布者实例
    public var projectedValue: Published<Value>.Publisher { mutating get set }
}

由上可知,@Published 的属性包装器里让属性持有了个自己声明的发布者。这样就可以让被@Published标记的属性自动生成发布者。

订阅也很简单:

loginStateCancel = viewModel.$loginState.sink { (state) in
    // 各种操作
}

使用 sink 方法订阅 viewModel.$loginState,这个 loginState 不是个枚举么...关键在 $ 符号上...这里的viewModel.$loginState 实际上返回的是:

Published<LoginVM.LoginState>.Publisher

一个发布者。通过$ 符号访问属性是获取属性包装器中的自定义属性 projectedValue 的值,在 @Published 中,这个自定义属性就是系统生成的发布者。关于属性包装器可以看看Property Wrappers

这里延伸出了一个问题: 每个需要绑定/观察的键都被 @Published 标记,然后又订阅,可当面对的是一个复杂的模型时就会产生大量重复操作。有没有...

当然有!使用 ObservableObject协议:

class LoginVM: ObservableObject {...}

ObservableObject 协议中定义了一个发布器,并在协议的扩展中实现了默认的发布器,这样就让遵循协议的类默认拥有了一个发布器,获取回调发布器的属性为objectWillChange

Tips:被 @Published 标记的属性更新前会回调,未被标记的属性则不会。

了解了这些,就可以通过 viewModel 的 objectWillChange 获取到发布者并订阅来监听所有被 @Published 标记的属性更改的回调

loginStateCancel = viewModel.objectWillChange.sink { [weak self]() in
    print("登录状态即将发生改变:\(self?.viewModel.loginState)")
}
啥也不说了

细心的你肯定发现 objectWillChange 返回的发布者,会在操作前回调,此时去获取属性还是旧值,查看协议后发现目前只有这么一个发布器,未来会不会推出objectDidChange不得而知,我们是等苹果还是自己动手实现一个更新后的发布者,甚至粗暴的加个异步延时呢?挖了个坑


总结

总的来说,可将 Combine 看作一种观察者模式,其分为 [发布者][订阅者] ,两者配合处理随着时间变化的值,还有操作符用来修改发布者的值。

Combine 在 WWDC19 上推出,而 WWDC20 上没有什么大的变动,倒是默默的推出了更多融合到系统架构中的功能。说明 Combine 的架构基本确定,未来也不会再有什么伤筋动骨的变动,以后只会新增更多的支持特性。

如果开发者的应用从 iOS13 为最低版本开发新应用的话,推荐使用 Combine 替代 ReactiveCocoa,RxSwift 等三方框架。

下一篇,深入一步,看看在 Combine 中如何解锁自定义的姿势。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容