Apple原生Rx框架Combine简介

Combine是什么

a declarative Swift API for processing values over time.

Combine是苹果推出的函数式Rective编程框架,和RxSwift,ReactiveObjC类似,主要用于处理时间变化的数据或者事件流。Apple的SDK中Combine是一个独立的Framework但是在许多其他的库中都有Combine的支持,比如SwiftUI就大量应用了Combine,其他的一些基础功能比如,NSNotificationCenter,URLSession和Timer也都有Combine的支持

Combine基本概念

函数用来返回一个值,Combine返回可能的多个值(基于时间序列)
函数返回错误或者抛出异常,Combine返回失败
Combine中有两种基本返回,正常输出和失败

Publisher,Subscriber

发布者(Publisher)的角色就是提供输出(output),当有值或者被请求就输出值,如果一个Publisher没有任何请求则被优化不做任何处理,Combine中提供两种基本输出,正常输出(output type)和失败(Failure)
与发布者(Publisher)对应的是订阅者(Subscriber),订阅者(Subscriber)订阅发布者的输出并进行处理。发布者(Publisher)和订阅者(Subcriber)构成Combine的核心概念。和Publisher对应的,Subscriber有一个输入类型(Input Type)和失败(Failure),关系如下图:

input_output.png

仅仅是Publisher和Subcriber还不够,我们可能需要对数据进行一定的处理再交给下一个,这就引入了另外一个概念,operator,operator同时支持了publisher和Subcriber协议,通过operator你可以订阅一个Publisher,接受它的输出处理后重新发布给其他Subcriber,组合起来就是这样:


image.png

操作者通常用来做数据类型变换,同时作用于输出类型和失败类型,操作者也可以用于分割输出,复制输出或者组合多个流。操作者的一个限制是输入和输出连接需要对齐,就像一个管道一样,所有连接处都必须一致。我们称这种组合方式为管道(Pipeline)

一个简单的示例:

import Combine
let _ = Just(5) (1)
.map { value -> String in  (2)
    // do something with the incoming value here
    // and return a string
    return "a string"
}
.sink { receivedValue in (3)
    // sink is the subscriber and terminates the pipeline
    print("The end result was \(receivedValue)")
}

(1) 管道起始于发布者Just,输出是5 <integer>类型,失败类型是<Never>
(2) 管道通过了map操作,返回了一个字符串,输出类型为<String>
(3) subcriber订阅了map后的输出,并打印了result。

发布者和订阅者的生命周期

Combine的设计是让终端操作可以完全控制数据流和管道处理流程,这是一种 back-pressure设计(可以理解为末端控制整个流程),这也意味着subscriber驱动订阅或者operator应该怎么输出数据。subscriber的请求通过通道(Pipeline)逐级向上链条传递。一个典型的例子就是cancel,当Subcriber要求取消的时候可中断链条上的所有操作。

image.png

也就是subscriber控制整个链条的生命周期

典型的Publisher

  • Just
    返回单个值并立即结束Publisher

  • Future
    创建一个异步的最终返回单个值或失败Publisher

  • @Published
    swift 语法糖,允许将任意属性转换为一个Publisher

  • Empty
    不返回任何值,然后直接结束<可选>

  • Sequence 将容器类型转变为值发布

  • Fail

    • 发送一次值更新,然后立即因错误而终止
    • 立即因错误而终止
  • Deferred
    Deferred 初始化需要提供一个生成 Publisher 的 closure,只有在有 Subscriber 订阅的时候才会生成指定的 Publisher,并且每个 Subscriber 获得的 Publisher 都是全新的。

  • ObservableObjectPublisher

Operators

  • scan
    scan 对每个信号都进行处理
     let pub = (0...5)
         .publisher
         .scan(0, { return $0 + $1 })
         .sink(receiveValue: { print ("\($0)", terminator: " ") })
      // Prints "0 1 3 6 10 15 ".
  • tryScan
    和上面类似,允许抛出错误
  • map
    经常使用,用来转换数据类型
    map还可以用来处理多个属性值拆分处理
    如果输出的value有多个属性,那么也可以用map和keypath来拆分:
[(x: 2, y: 3), (x: 1, y: 5), (x: 2, y: 6)].publisher
    .map(\.x, \.y)
    .sink(receiveCompletion: { print($0) },
        receiveValue: { x, y in print(x + y) })
    .store(in: &subscriptions)
5
6
8
finished
  • tryMap 和tryScan类似
  • flatMap
    如果你有一个publisher发送的value的某些属性也是publisher, 那sink只会receive外面这个publisher发出的value,属性publisher发出的value要是也想接收,那你就可以用flatMap
.flatMap { data in
    return Just(data)
    .decode(YourType.self, JSONDecoder())
    .catch {
        return Just(YourType.placeholder)
    }
}

flatmap常用于异常处理,在我们处理信号时,有时候部分信号代表错误,而你希望做catch处理后给一个默认值就很有用了,像上面一样。

  • reduce
    与scan不同的时,publisher一定要结束才会输出最后值
[1, 2, 3].publisher
    .reduce(0, +)
    .sink(receiveCompletion: { print($0) },
        receiveValue: { print($0) })
    .store(in: &subscriptions)

6
finished
  • filter 筛选出过滤的信号
[1, 2, 3].publisher
    .filter { $0 < 2 }
    .sink(receiveCompletion: { print($0) },
        receiveValue: { print($0) })
    .store(in: &subscriptions)

1
finished

类似的还有fist,last,drop,dropFirst, prefix

Subscriber

Combine 内置的 Subscriber 有三种:

  • Sink
    通用的closure方式处理Publisher的值
let once: Publishers.Once<Int, Never> = Publishers.Once(100)
let observer: Subscribers.Sink<Publishers.Once<Int, Never>> = Subscribers.Sink(receiveCompletion: {
    print("completed: \($0)")
}, receiveValue: {
    print("received value: \($0)")
})
once.subscribe(observer)

// received value: 100
// completed: finished
  • Assign
    Assign 可以很方便地将接收到的值通过 KeyPath 设置到指定的 Class 上(不支持 Struct),很适合将已有的程序改造成Reactive。
    例如:
class Student {
    let name: String
    var score: Int

    init(name: String, score: Int) {
        self.name = name
        self.score = score
    }
}

let student = Student(name: "Jack", score: 90)
print(student.score)
let observer = Subscribers.Assign(object: student, keyPath: \Student.score)
let publisher = PassthroughSubject<Int, Never>()
publisher.subscribe(observer)
publisher.send(91)
print(student.score)
publisher.send(100)
print(student.score)

// 90
// 91
// 100

一单Publisher发送新的值,Student的score属性也会随着发生变化。

  • Subject
    有些时候我们想随时在 Publisher 插入值来通知订阅者,在 Combine 中也提供了一个 Subject 类型来实现。Subject 通常是一个中间代理,即可以作为 Publisher,也可以作为 Observer。Subject要求实现一个send方法,允许向combine链条发送值。
    Combine有两个内置的Subject类型 CurrentValueSubjectPassthroughSubject
    CurrentValueSubject 的功能很简单,就是包含一个初始值,并且会在每次值变化的时候发送一个消息,这个值会被保存,可以很方便的用来替代 Property Observer。
// Before
class ContentManager {

    var content: [String] {
        didSet {
            delegate?.contentDidChange(content)
        }
    }

    func changeContent() {
        content = ["hello", "world"]
    }
}

// After
class RxContentController {

    var content = CurrentValueSubject<[String], NSError>([])

    func changeContent() {
        content.value = ["hello", "world"]
    }
}

PassthroughSubject 和 CurrentValueSubject 几乎一样,只是没有初始值,也不会保存任何值。

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

推荐阅读更多精彩内容

  • 在具体介绍 Combine 之前,有两个重要的概念需要简要介绍一下: 观察者模式 响应式编程 观察者模式 观察者模...
    没八阿哥的程序阅读 8,969评论 2 21
  • 简介 Combine是Apple在2019年WWDC上推出的一个新框架。该框架提供了一个声明性的Swift API...
    云天涯丶阅读 24,415评论 5 22
  • 前言 之前对RAC有了一个基本的认识,了解了它的作用,以及RAC的运行机制,我们知道只要是信号(RACSignal...
    大大盆子阅读 4,495评论 0 11
  • 1.ReactiveCocoa常见操作方法介绍1.1 ReactiveCocoa操作须知所有的信号(RACSign...
    IIronMan阅读 2,585评论 2 17
  • 1.ReactiveCocoa常见操作方法介绍。 1.1 ReactiveCocoa操作须知 所有的信号(RACS...
    萌芽的冬天阅读 1,015评论 0 5