什么是Combine
“一套统一的声明性API,用于处理随时间变化的值,其有着支持泛型,类型安全,组成优先,请求驱动的特点”
这是 WWDC19 上苹果推出 Combine 时的官方描述。在 iOS 的开发者社区中基本都将其与 响应式编程 挂钩。如 OC 下的 ReactiveCocoa 与 Swift 下的 Rx 套件(RxSwift、RxCocoa等),这些都是响应式编程框架。
其他第三方响应式编程框架不香吗?开发中引入第三方框架,也就等于引入了一定风险(Bug、性能缺陷、停止维护、甚至巨大的代码量) 。Combine 的优势就是 “官方出品” ,意味着它能进行系统底层优化,更不用说其与 Swift、UIKit、SwiftUI 等官方框架的深度融合。所以了解 Combine 是非常必要的。
Combine 的组成
Combine 的结构跟其他响应式框架类似,其中最基础的组成分为三个部分,简单说明如下:
Publisher(发布者)
值类型,描述了 值
和 错误
是如何产生的,遵循 Publisher 协议
,协议中声明了值类型与错误类型(Output
与 Failure
),声明 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 中如何解锁自定义的姿势。