SwiftUI - Combine

前言

“一个随时间处理数据的声明式的 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 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容