SwiftUI - Combine
前言
“一个随时间处理数据的声明式的 Swift API。”Combine 苹果采用的一种函数响应式
编程的库,类似于 RxSwift
。Combine
使用了许多在其他语言和库中可以找到的相同的函数响应概念,并将Swift的静态类型特性应用到其解决方案中。
像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。
SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源
的管理。
响应式编程的核心是将所有事件转化成为异步的数据流
,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。
1、Pulishers、Operators、Subscribers
Pulishers
:发布者,负责提供数据(当数据可用且获得请求)。一个发布者如果没有订阅,则不会发布任何数据。当你在描述一个发布者时,你会用两种相关类型(associatedtype)来表述他:Output
和 Failure
。比如发布者返回 String实例 ,并且可能以 URLError实例 的形式返回失败,那么发布者可以用 <String, URLError>
来描述。
Subscribers
:订阅者,负责(向发布者)请求数据和接收发布者提供的数据(或者失败信息)。订阅者用两种相关类型进行描述:Input
和 Failure
。订阅者发起数据请求,并空值接收到的数据量。在 Combine 中,他可以看作是“行为的驱动者”,没有了订阅者,其他的组成部分将闲置。
发布者和订阅者是相互连接的,并构成 Combine 的核心。当你连接一个订阅者到发布者上,Input 和 Output 类型必须一致,两者的 Failure 也需要一致。
Operators
:操作者是一个行为类似订阅者和发布者的对象。他既实现了 Publisher
协议 ,又实现了 Subscriber
协议 。他们支持订阅一个发布者,并接收订阅者的请求。
一般的数据流是这样处理的:发布者 -> 操作者1 -> 操作者2 -> ... -> 操作者n -> 订阅者
操作者可以被用来转换数值或者值的类型 -- Output 和 Failure 均可。操作者也可以分割、复制、合并数据流。操作者之间的 Output/Failure类型 必须一致,否则编译器会报错。
2、Future、Promise
Future:未来某个时刻会发布一个数据,会立即结束,并且会带有一个状态,是成功还是失败的状态。(类似我们Swift中的逃逸闭包
)
final public class Future<Output, Failure> : Publisher where Failure : Error {
public typealias Promise = (Result<Output, Failure>) -> Void
public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}
查看源码,包含一个Promise类型及一个逃逸闭包的初始化函数。Future和Promise结合使用,一个未来要给的承诺,也就是未来执行的操作返回的一个最终结果。初始化函数中可以看出Promise为接收单个Result类型的闭包。
拓展:
原理上Future和PassthroughSubject、CurrentValueSubject很类似,Future遵循Publisher协议,后两者遵循的是Subject协议,可以直接使用send方法发送数据。
3、简单示例
创建一个Future类型的闭包任务(发布者),即一个将会在未来某时刻调用的闭包,闭包会返回字符串3,没有错误返回,:
let futurePublisher = Future<String, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success("3"))
}
}
新增ViewModel数据管理类,遵循ObservableObject协议,即被@Published修饰符修饰的属性title改变时,就会发布通知给使用到此属性的View刷新。
extension ContentView {
class ViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
//刷新视图用的变量
@Published var title: String = "Hello Lcr"
func fetchData() {
futurePublisher.print("_fetchData_")
.receive(on: RunLoop.main)
.sink { completion in
switch completion {
case .failure(let err):
print("Error is \(err.localizedDescription)")
case .finished:
print("Finished")
}
} receiveValue: { [weak self] data in
print("fetchWebData: \(data)")
self?.title = data
}
.store(in: &cancellables)
}
}
}
关于futurePublisher.sink{} receiveValue{}函数,就是在订阅者和发布者之间桥梁,以及后续接收数据操作。
Use
Publisher/sink(receiveCompletion:receiveValue:)
to observe values received by the publisher and process them using a closure you specify.
订阅者可以通过sink函数响应调用,block区域将会收到publisher发出的values,publisher可以发射0个或多个values,除了基本值之外,您的publisher还会发给订阅者特殊值。如.finished(完成)、.failure()(失败)。
struct ContentView: View {
@StateObject var vm = ViewModel()
var body: some View {
Text(vm.title).padding().onAppear{
vm.fetchData()
}
}
}
订阅者Text通过vm操作者去向发布者索要数据,futurePublisher闭包会执行,2秒后将Promise闭包执行将数据返回,receiveValue接收到数据,保存至cancellables,状态为finished,即任务到此结束。
combine简单示例
4、backPresssure
对于大多数响应式编程场景而言,订阅者不需要对发布过程进行过多的控制。当发布者发布元素时,订阅者只需要无条件地接收即可。但是,如果发布者发布的速度过快,而订阅者接收的速度又太慢,我们该怎么解决这个问题呢?Combine 已经为我们制定了稳健的解决方案!现在,让我们来了解如何施加背压(back pressure,也可以叫反压)以精确控制发布者何时生成元素。
在 Combine 中,发布者生成元素,而订阅者对其接收的元素进行操作。不过,发布者会在订阅者连接和获取元素时才发送元素。订阅者通过 Subscribers.Demand 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。
订阅者可以通过两种方式来表明需求(Demand):
- 调用 Subscription 实例(由发布者在订阅者进行第一次订阅时提供)的 request(_:) 方法;
- 在发布者调用订阅者的 receive(_:) 方法来发送元素时,返回一个新的 Subscribers.Demand 实例;
下面利用一个简单例子演示一下:
let width = UIScreen.main.bounds.width, height = UIScreen.main.bounds.height
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton.init(frame: CGRect.init(x: (width-180)/2, y: 420, width: 180, height: 40))
button.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside)
button.setTitle("订阅 timerPublisher", for: .normal)
button.backgroundColor = .orange
button.layer.cornerRadius = 10
view.addSubview(button)
}
@objc func tapped(button: UIButton) {
// 订阅
print ("开启订阅 \(Date())")
timerPub.subscribe(MySubscriber())
}
}
// 发布者: 使用一个定时器来每秒发送一个日期对象
let timerPub = Timer.publish(every: 1, on: .main, in: .default).autoconnect()
// 订阅者: 在订阅以后,等待2秒,然后请求最多3个值
class MySubscriber: Subscriber {
// typealias Input = Date
// typealias Failure = Never
// var subscription: Subscription?
func receive(subscription: Subscription) {
print("订阅接收到了")
// self.subscription = subscription
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
subscription.request(.max(3))
}
}
func receive(_ input: Date) -> Subscribers.Demand {
print("发布时间:\(input)——————接收时间:\(Date())")
return Subscribers.Demand.none
}
func receive(completion: Subscribers.Completion<Never>) {
print("完成")
}
}
struct ContentView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
后压结果
可见订阅者通过 Subscribers.Demand 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。