SwiftUI - Combine

作者: Lcr111 | 来源:发表于2023-02-28 11:22 被阅读0次

    前言

    “一个随时间处理数据的声明式的 Swift API。”Combine 苹果采用的一种函数响应式编程的库,类似于 RxSwiftCombine 使用了许多在其他语言和库中可以找到的相同的函数响应概念,并将Swift的静态类型特性应用到其解决方案中。

    像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。

    SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。

    响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。

    1、Pulishers、Operators、Subscribers

    Pulishers:发布者,负责提供数据(当数据可用且获得请求)。一个发布者如果没有订阅,则不会发布任何数据。当你在描述一个发布者时,你会用两种相关类型(associatedtype)来表述他:OutputFailure 。比如发布者返回 String实例 ,并且可能以 URLError实例 的形式返回失败,那么发布者可以用 <String, URLError> 来描述。

    Subscribers:订阅者,负责(向发布者)请求数据和接收发布者提供的数据(或者失败信息)。订阅者用两种相关类型进行描述:InputFailure 。订阅者发起数据请求,并空值接收到的数据量。在 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 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。

    相关文章

      网友评论

        本文标题:SwiftUI - Combine

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