Combine
通过绑定事件处理(event-progressing)操作符来自定义处理异步事件
总览
Combine框架提供了一种声明式的Swift API, 用来随时处理各种值。这些值可以被当做各种各样的异步事件。Combine声明了发布者来公开那些随时可能变化的值。并且订阅者接收这些来自发布者的值。
Publisher
协议声明了一种可以随时传递一系列值的类型。发布者有operaters(操作符)根据从上游发布者收到的值采取行动并重新发布它们。- 在发布者的链条终端,会有一个
Subscriber
根据它所接收到的元素来做出反应。发布者只在订阅者明确要求时才发布值。这使你的订阅者代码可以控制从与其连接的发布者那里接收事件的速度。
多种Foundation类型都是通过发布者来公开他们的功能,包括 Timer
,NotificationCenter
和 URLSession
。Combine
同时也为符合KVO(键值观察)的所有属性提供了内置发布者。
你可以绑定多个发布者的输出,并使其交互。例如,你可以订阅一个输入框(textField)的发布者来进行更新操作,用其文本值来执行一段网络请求。你后续也可以用其他发布者来执行请求的响应来更新APP。
采用了 Combine
之后,通过集中事件处理和排查问题的技术像嵌套闭包以及基于会话的回调来使你的代码将会变得更加易读和维护。
引子
接下来我们来看一个关于《利用Combine来接收和处理事件》的讨论。
概述
Combine
提供了一种声明式的途径来让你的APP处理各种事件。跟以前执行多个代理回调或者completionHandler闭包相比,亦可以为一个给定事件源创建一个执行链。链的每一部分都是由处理从上一步接收到的指定行为和元素的操作符组成的。
想象一下有一个App需要根据一个textField的text来适配一个tableView和collectionView,在APPKit中,每在textfield中键入一个字符,都会产生一个Notification
,你可以用Combine来对这个Notification
进行订阅。在收到通知后,你可以用操作符来改变事件传递的内容和计时,也可以用最后的结果来更新APP的用户界面。
连接Publisher和Subscriber
为了能用Combine来接收textfield的通知,访问NotificationCenter
实例并调用他的publisher(for: object:)
方法。这个调用方法携带通知名和资源对象,并且会返回一个产生通知元素的发布者。
let pub = NotificationCenter.default.publisher(for: NSControl.textDidChangeNotification, object: filterField)
用Subscriber
来接收来自发布者的元素。订阅者定义了一个关联类型,Input
, 来声明他所接收的类型。订阅者同样也会声明一个类型,Output
,来声明他产生的元素的类型。发布者和订阅者都会定义一种类型,Failure
,来代表他们产生或者接收的错误。想要将订阅者对接到发布者,Output
必须和Input
必须相匹配,同样Failure
类型也要相匹配.
Combine提供了两种内嵌的订阅者,这些订阅者会自动将属于他们的发布者的output和failure。
sink(receiveCompletion:receiveValue)
携带两个闭包。第一个闭包在接收到Subcribers.Completion
时执行,这个回调是一个代表着publisher正常结束或者发生错误。第二个闭包在他接收到来自publisher的元素时执行。assign(to:on:)
会立即分发每个从一个既定对象的属性收到的元素,通过key-path来标示这个属性。
举个例子,你可以用sink
订阅者来在发布者完成时/每次收到元素时进行打印:
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.sink(receiveCompletion: { print ($0) }, receiveValue: { print ($0) })
sink(receiveCompletion:receiveValue)
和assign(to:on:)
两者都从他们的publisher那里请求了大量的元素,这些元素的数量是不受限的。想要控制接收元素的频率,可以通过实现Subscriber
协议来创建你自己的订阅者。
通过Operators来改变Output类型
前述的sink
订阅者都是在receive
闭包中执行他的业务。如果需要通过接收到的元素或者需要维持两者的调用来执行大量自定义业务,这一点就很烦人了。Combine的先进之处就在于他提供了大量的操作符来处理自定义事件传递。
比如, NotificationCenter.Publisher.Output,在接收比如来自textfield的字符串时,它并不是一个非常方便的类型。因为publisher的output本质上来讲是一个随时的元素序列, Combine提供了序列修改操作符,比如 map(_:)
, flatMap(maxPublishers:_:)
, 和reduce(_:_:)
。这些操作符与他们在Swift标准库中的对应体相似。
你可以添加一个map(_:)
操作符,这个操作符返回一个不同类型,来改变这个发布者的output。在这个案例中,你可以获取一个NSTextField
类型的通知对象,然后获取这个textField的stringValue
。
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .sink(receiveCompletion: { print ($0) }, receiveValue: { print ($0) })
在发布者链产生了你想要的类型之后,用assign(to:on:)
取代sink(receiveCompletion:receiveValue:)
。下面的例子懈怠了一个从发布者链接收到的字符串,并且将他们分发给一个自定义ViewModel对象的filterString:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel)
自定义发布者和操作符
您可以使用一个运算符来扩展Publisher实例,该运算符执行否则需要手动编码的操作。以下是可以使用运算符来改进此事件处理链的三种方法:
-
您可以使用
filter(_:)
运算符来忽略特定长度的输入或拒绝非字母数字字符,而不是使用键入到文本字段中的任何字符串来更新视图模型。 -
如果过滤操作很耗时(例如,如果它查询一个大型数据库),您可能需要等待用户停止键入。为此,
debouce(for:scheduler:options:)
运算符允许您设置发布者发出事件之前必须经过的最短时间段。RunLoop
类提供了以秒或毫秒为单位指定时间延迟的便利。 -
如果结果更新了UI,则可以通过调用
receive(on:options:)
方法将回调传递给主线程。通过将RunLoop类提供的Scheduler实例指定为第一个参数,可以告诉Combine在主运行循环上调用订阅者。
声明如下:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } ) .debounce(for: .milliseconds(500), scheduler: RunLoop.main) .receive(on: RunLoop.main) .assign(to:\MyViewModel.filterString, on: myViewModel)
在需要时取消发布
发布者持续发出元素,直到正常结束或失败。如果你不想再订阅发布者,一你一取消订阅。订阅者类型可以通过sink(receiveCompletion:receiveValue:)
和assign(to:on:)
来执行Cancellable
协议,这个协议提供了取消方法:
sub?.cancel()
如果你创建了一个自定义的Subscriber
,发布者会在你第一次订阅他的时候发送一个 Subscription
对象。存储这个订阅,并在要结束发布的时候调用cancel()
方法。当你创建了一个自定义的订阅者,你应该执行Cancellable
协议,并且让cancel()
方法来实现存储的订阅。