美文网首页iOS开发技术博客
iOS Combine - 1.初见 Combine

iOS Combine - 1.初见 Combine

作者: 猴子的饼干 | 来源:发表于2020-08-26 11:15 被阅读0次

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

    相关文章

      网友评论

        本文标题:iOS Combine - 1.初见 Combine

        本文链接:https://www.haomeiwen.com/subject/vxuzjktx.html