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(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)
}

订阅者订阅发布者后会返回一个遵循 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 {

    public init(wrappedValue: Value)
    public init(initialValue: Value)

    // 发布者定义
    public struct Publisher : Publisher {
        public typealias Output = Value
        public typealias Failure = Never

        public func receive(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published.Publisher.Failure
    }

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

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

订阅也很简单:

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

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

Published.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 中如何解锁自定义的姿势。

你可能感兴趣的:(iOS Combine - 1.初见 Combine)