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),关系如下图:
仅仅是Publisher和Subcriber还不够,我们可能需要对数据进行一定的处理再交给下一个,这就引入了另外一个概念,operator,operator同时支持了publisher和Subcriber协议,通过operator你可以订阅一个Publisher,接受它的输出处理后重新发布给其他Subcriber,组合起来就是这样:
操作者通常用来做数据类型变换,同时作用于输出类型和失败类型,操作者也可以用于分割输出,复制输出或者组合多个流。操作者的一个限制是输入和输出连接需要对齐,就像一个管道一样,所有连接处都必须一致。我们称这种组合方式为管道(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要求取消的时候可中断链条上的所有操作。
也就是subscriber控制整个链条的生命周期
典型的Publisher
Just
返回单个值并立即结束PublisherFuture
创建一个异步的最终返回单个值或失败Publisher@Published
swift 语法糖,允许将任意属性转换为一个PublisherEmpty
不返回任何值,然后直接结束<可选>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类型CurrentValueSubject
和PassthroughSubject
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 几乎一样,只是没有初始值,也不会保存任何值。