SwiftUI - 基础到实战

SwiftUI - Combine

2023-02-28  本文已影响0人  Lcr111

前言

“一个随时间处理数据的声明式的 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):

下面利用一个简单例子演示一下:

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 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。

上一篇 下一篇

猜你喜欢

热点阅读