Combine | (I) Hello, Combine!
Combine 简介
Combine 概述
Combine framework 供给了一个声明式的 Swift API,经过 Combine,咱们能够为给定的作业源创立单个处理链,而不是完结多个 delegate callback 或 completion handler。链的每个部分是一个 Combine 的操作符(Operator),它对从上一步接纳到的值履行不同的操作。这些值能够表示多种异步作业,发布者(Publisher) 能够发布随时刻改变的值,订阅者(Subscriber) 从发布者那里接纳这些值。
经过运用 Combine,咱们会集咱们的作业处理代码,消代码中的嵌套的闭包、依据约定的回调等技能,使咱们的代码更易于阅览和保护。该系列将介绍 Combine framework,并运用 Swift 编写声明式(Declarative)、呼应式(Reactive) App。
Apple 渠道下的异步编程
Apple 也在不断在改善其渠道的异步编程,咱们能够运用多种机制创立和履行异步代码。咱们必定运用过这些内容:NotificationCenter
、GCD、Operation
、托付形式和闭包等。在这些技能布景下,编写高质量的异步代码会更复杂一些,不同类型的异步 API,每个都有自己的接口规划方案。
Combine 作为一种通用的规划和编写异步代码的高级(High-level)言语被引进 Swift,也被 Apple 集成到 Timer、NotificationCenter 和 Core Data 等结构中。从 Foundation 一向到 SwiftUI,Apple 将 Combine 集成作为其“传统”API 的代替方案。作为开发者,Combine 也很容易集成到咱们自己的代码中。
声明式(Declarative)、呼应式(Reactive)编程已经存在了很长一段时刻。在 2009 年微软推出的 .NET (Rx.NET) 是第一个的呼应式方案。2012 年其开源后,许多不同的言语开始运用这一概念。在 Apple 渠道,已经有几个第三方呼应式结构,如 RxSwift,它完结了 Rx 规范。
Combine 完结了一个与 Rx 不同但相似的规范,称为 Reactive Streams。 Reactive Streams 与 Rx 有一些要害区别,但它们有一起的中心概念。在 iOS 13/macOS Catalina 及以后,Apple 经过内置的 Combine 结构为其生态带来了呼应式编程的支持。
Combine 的根底概念
Combine 中的四个要害部分是发布者(Publisher)、操作符(Operator) 和 订阅者(Subscriber),以及订阅(Subscription)。
发布者(Publisher)根底概念
Publisher 能够跟着时刻的推移向一个或多个接纳方(订阅者)宣布值的类型。每个 Publisher 都能够宣布这三种类型的多个作业:
-
该 Publisher 的
Output
类型的值; -
表示成功的 completion;
-
带有该 Publisher 的
Failure
类型的Error
的 completion。
Publisher 能够宣布零个或多个 Output
类型的值,假如它完结了,无论是成功仍是失利,后续它都不会宣布任何其他作业。
以发布 Int
值的 Publisher 在时刻轴上的可视化效果为例:
蓝色框表示在时刻线上给定时刻 Publisher 宣布的值。图表右竖线表示 stream 的成功完结。三种类十分普遍,代表了咱们 App 中的任何类型的动态数据。
Publisher 协议有两种类型是通用的,正如前文提到:
-
Publisher.Output
是 Publisher 输出的值的类型。 一个Int
Publisher 永远不能直接宣布String
类型值。 -
Publisher.Failure
是 Publisher 在失利时能够抛出的过错类型。 假如 Publisher 永远不会失利,咱们能够指定为Never
类型来。
当咱们订阅某个 Publisher 时,会希望从中获得什么值以及或许会因哪些过错而失利,即 Publisher.Output
与 Publisher.Failure
。
操作符(Operator)根底概念
Operator 是在 Publisher 协议上声明的办法,它们回来相同的或新的 Publisher。咱们能够一个接一个地调用操作符,然后有效地将它们链接在一起。这些操作符是高度解耦、可组合的,所以它们能够组合起来在单个订阅(Subscription) 中完结复杂逻辑。操作符总的输入和输出,一般称为 上游(Upstream) 和 下游(Downstream)。但需要注意,假如一个 Operator 的输出与下一个 Operator 的输入类型不匹配,则不能组合在一起。
操作符专心于处理从前一个操作符接纳到的数据,并将其输出供给给链中的下一个操作符。咱们能够以清晰的办法,来界说每个异步的、笼统的作业的次序,以及有清晰的输入、输出类型和过错处理。
订阅者(Subscriber)根底概念
到达订阅链的结尾,每个订阅都以一个 Subscriber 结束。Subscriber 一般对输出的值或作业做些什么。
目前,Combine 供给了两个内置 Subscriber:
-
Sink Subscriber 答应咱们运用闭包。来接纳值或作业。在那里咱们能够对值或作业做想做的作业。
-
Assign Subscriber 答应咱们经过 keypath 直接将成果绑定到模型,或 UI 控件上的某个特点然后在直接屏幕上显现数据。
假如咱们对数据有其他需求,创立自界说 Subscriber 比创立 Publisher 更容易。 Combine 供给十分简略的协议,使咱们能够适宜的构建自己的工具。
订阅(Subscription)根底概念
订阅(Subscription) 值 Combine 的 Subscription 协议及完结该协议的目标,通俗的说,是 Publisher、Operator 和 Subscriber 的完好链。
当咱们在 Subscription 的结尾增加 Subscriber 时,它会在链的开头“激活” Publisher。即假如没有 Subscriber 接纳输出,则 Publisher 不会宣布任何值。
Subscription 答应咱们运用自己的自界说代码、过错处理,声明晰一连串异步作业。而且这些代码咱们只需要做一次,然后就不用再考虑它了。
假如咱们的 App 彻底运用 Combine,能够经过 Subscription 来描绘咱们的整个 App 的逻辑。这样咱们不需要再写 Push Data 或 Pull Data 或 callback 之类的代码:
此外,咱们不需要专门办理 Subscription:Combine 供给的一个名为 Cancellable
的协议。
系统供给的两个 Subscriber 都契合 Cancellable
,这意味着咱们的 Subscription 代码(整个 Publisher、Operator 和 Subscriber 调用链)回来一个 Cancellable 目标。每逢咱们从内存中开释 Cancellable
目标时,它都会撤销整个 Subscription 并从内存中开释其资源。
因而,咱们能够经过绑定 Subscription 的生命周期到 ViewController 的 strong 特点中,每逢用户从封闭 ViewController 时,都会析构其特点并撤销 Subscription。咱们能够自动化这个过程,增加一个 [AnyCancellable]
类型的特点,并在其间增加当时的 Subscription。当该特点从内存中开释时,这些 Subscription 都会被自动撤销并开释。
Combine 的优势
当时,不运用 Combine 仍能够创立好的 App。可是运用这些结构会更方便、安全和高效。系统级其他异步代码的笼统,其意味着已经经过充沛测验、有更紧密集成和更安全的技能:
- Combine 在系统级别上集成,自身运用了一些不揭露的言语功用,供给了咱们无法自己构建的 API;
- Combine 将许多常见操作笼统为 Publisher 协议上的办法,包括内置的 Operator ,已经过 Apple 的测验;
- 当咱们的代码中一切的异步作业都运用 Publisher 的接口,模块组合和可重用性变得十分强大。
- Combine 的 Operator 是高度可组合的。假如咱们创立一个新的 Operator,其与其他的 Combine 即插即用。
此外,在 App 架构方面,Combine 必定不是一个影响咱们怎么构 App 的结构。Combine 仅仅处理异步数据和作业的通信协议,它不会改变其他内容。咱们能够在你的 MVC 中运用 Combine,相同能够在 MVVM 代码、VIPER 等中运用它。因而,咱们能够迭代地和有挑选地增加 Combine 代码,咱们不需要做出的“全有或全无”的挑选。例如咱们能够首要转换咱们的数据模型,或调整网络层,再或许只为新增代码中运用 Combine。
假如咱们同时选用 Combine 和 SwiftUI,状况会略有不同。SwiftUI 从 MVC 架构中删去了 C,也要归功于将 Combine 和 SwiftUI 的结合运用。当咱们从数据模型到视图一向运用呼应式编程时,其实不需要一个特别的 Controller 来操控视图:
发布者(Publisher)
NotificationCenter
Combine 的中心是 Publisher 协议。 该协议界说了对 Publisher 类型的要求,使其能够随时刻将一系列值传输给一个或多个订阅者。
订阅 Publisher 的想法相似于订阅来自 NotificationCenter
的通知。Apple 也在 NotificationCenter
供给了 publisher(for:object:)
办法,回来一个能够发布通知的 Publisher。
能够尝试在 Playground 中以下代码:
import Foundation
let center = NotificationCenter.default
let myNotification = Notification.Name("MyNotification")
let publisher = center.publisher(for: myNotification, object: nil)
let observer = center.addObserver(
forName: myNotification,
object: nil,
queue: nil) { notification in
print("Notification received!")
}
center.post(name: myNotification, object: nil)
center.removeObserver(observer)
在上述代码中,咱们经过 center
获取了一个发布 myNotification
类型的 Notification
的 Publisher。接着,咱们创立一个 observer
来监听 center
的 myNotification
类型的 Notification
。在收到该 Notification
时,将打印 Notification received!
。终究,咱们在 center
上 post myNotification
。终究,操控台将展示:
Notification received!
但上述输出,实际上并非来自 Publisher,咱们持续往下看。
订阅者(Subscriber)
运用 sink(_:_:)
从头调整 Playground 中的代码:
import Foundation
let center = NotificationCenter.default
let myNotification = Notification.Name("MyNotification")
let publisher = center.publisher(for: myNotification, object: nil)
let subscription = publisher
.sink { _ in
print("Notification received from a publisher!")
}
center.post(name: myNotification, object: nil)
subscription.cancel()
咱们经过在 publisher
上运用 sink(_:_:)
创立 Subscription,在 publisher
发布 myNotification
值时,将打印内容:
Notification received from a publisher!
检查 sink
,咱们会看到它其实是一个简略的办法,Subscriber 经过闭包处理以处理来自 Publisher 的 Output
类型的值,sink
将持续接纳与 Publisher 宣布的值,这也称为无限需求(Unlimited demand)。:
func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
此外, sink
实际上供给了两个闭包:一个用于处理接纳的值,另一个用于处理接纳成功或失利的 completion 作业。在 Playground 上增加 import Combine
后持续增加代码:
let just = Just("Hello, Combine!")
_ = just
.sink(
receiveCompletion: {
print("Received completion: ", $0)
},
receiveValue: {
print("Received value: ", $0)
})
咱们在这里运用 Just
创立 Publiesher, Just
答应咱们以单个值创立 Publiesher。接着创立对 Publiesher just
的 Subscription,并为接纳到的 completion 作业和值进行打印:
Received value: Hello, Combine!
Received completion: finished
咱们来看下 Just
的描绘,它是一个 Publisher,它向每个 Subscriber 宣布一次 output(值),然后 finish(completion 作业):
A publisher that emits an output to each subscriber just once, and then finishes.
咱们能够持续添代码,增加另一个 Subscriber:
_ = just
.sink(
receiveCompletion: {
print("Received completion (another): ", $0)
},
receiveValue: {
print("Received value (another): ", $0)
})
终究,操控台将输出以下内容:
Received value: Hello, Combine!
Received completion: finished
Received value (another): Hello, Combine!
Received completion (another): finished
运用 assign(to:on:)
除了 sink
之外,内置的 assign(to:on:)
运算符能够将接纳到的值分配给目标的特点,而且与 KVO 兼容。能够在 Playground 中删去之前的代码,并增加以下代码:
class MyObject {
var value: String = "" {
didSet {
print(value)
}
}
}
let object = MyObject()
let publisher = ["Hello", "Combine!"].publisher
_ = publisher
.assign(to: \.value, on: object)
在这里,咱们首要界说了一个具有 value
特点的 MyObject
类,value
在 didSet
后,将打印当时 value
。
创立 MyObject
类的实例 object
。从 String
数组创立 publisher
,订阅该 publisher
,将收到的值分配给 object
的 value
特点。运转 Playground,操控台将终究展示:
Hello
Combine!
assign(to:on:)
在处理 UIKit 或 AppKit 结构的 App 时特别有用,咱们能够将值直接分配给label
、button
等 UI 组件。
运用 assign(to:)
assign
还一个变体, assign(to:)
可将 Publisher 宣布的值用于 @Published
特点包装器注解的特点,在 Playground 中删去之前的代码,并增加以下代码:
class MyObject {
@Published var value = 0
}
let object = MyObject()
object.$value
.sink {
print($0)
}
(0..<5).publisher
.assign(to: &object.$value)
咱们界说 MyObject
类,并创立一个 object
实例,value
特点用 @Published
特点包装器注解,除了可作为惯例特点拜访之外,它还为特点创立了一个 Publisher。运用 @Published
特点上的 $
前缀来拜访其底层 Publisher,订阅该 Publisher,并打印出收到的每个值。终究,咱们创立一个 0..<5 的 Int
Publisher 并将它宣布的每个值 assign
给 object
的 value
Publisher。 运用 &
来表示对特点的 inout 引证,这里的 inout 来源于函数签名:
func assign(to published: inout Published<Self.Output>.Publisher)
这里有一些差异,assign(to:)
不回来 AnyCancellable
,在内部完结了生命周期的办理,在 @Published
特点开释时会撤销订阅。终究,操控台将输出:
0
1
2
3
4
咱们或许想知道运用 assign(to:on:)
和 assign(to:)
还有哪些差异?咱们检查以下代码:
class MyObject {
@Published var value: String = ""
var subscriptions = Set<AnyCancellable>()
init() {
["A", "B", "C"].publisher
.assign(to: \.value, on: self)
.store(in: &subscriptions)
}
}
这这里,运用 assign(to: \.word, on: self)
并存储生成的 AnyCancellable
,这会导致引证循环:MyObject
类实例持有生成的 AnyCancellable
,而生成的 AnyCancellable
相同持有MyObject
类实例。此刻用 assign(to:)
替换 assign(to:on:)
能够避免引进这个问题。
Cancellable
当 Subscriber 不再希望从 Publisher 接纳值时,咱们需要撤销 Subscription,开释资源并中止产生任何不应该触发的作业。
Subscription 将 AnyCancellable
实例作为用于撤销 Subscription 的 token 回来,因而咱们能够在完结后撤销 Subscription。 AnyCancellable
契合 Cancellable
协议,该协议正是为此意图,供给 cancel()
办法。在前面的示例中,咱们能够在终究增加 subscription.cancel()
来撤销 Subscription。
假如咱们没有显式调用 cancel()
,它将一向持续到 Publisher 宣布 completion 作业,或直到正常的内存办理导致存储的Subscription 开释。
Subscription 中的相互作用
咱们先来解释一下 Publisher 和 Subscriber 之间的相互作用,以下是一个简略概述:
-
Subscriber 订阅 Publisher;
-
Publisher 创立 Subscription 并将其供给给 Subscriber;
-
Subscriber 请求值;
-
Publisher 发送对应数量的值;
-
Publisher 发送 completion 作业。
咱们来看下 Publisher
协议:
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S)
where S: Subscriber,
Self.Failure == S.Failure,
Self.Output == S.Input
}
extension Publisher {
public func subscribe<S>(_ subscriber: S)
where S : Subscriber,
Self.Failure == S.Failure,
Self.Output == S.Input
}
-
Output
是 Publisher 生成的值的类型; -
Failure
是 Publisher 或许产生的过错类型,假如 Publisher 保证不会产生过错,则为Never
; -
先看 extension 中的办法,Subscription 时,Subscriber 在 Publisher 上调用
subscribe(_:)
; -
回过头看
receive(subscriber:)
,刚刚的subscribe(_:)
的完结将调用receive(subscriber:)
,将 Subscriber 附加到 Publisher 上,即创立 Subscription。
Subscriber 有必要匹配 Publisher 的 Output
和 Failure
才能创立订阅。咱们接着看看 Subscriber 协议:
public protocol Subscriber: CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
-
Input
是 Subscriber 能够接纳的值的类型; -
Failure
是 Subscriber 能够接纳的过错类型; 假如 Subscriber 不会收到过错则为Never
; -
Publisher 在 Subscriber 上调用
receive(subscription:)
来给 Subscriber 回来 Subscription; -
Publisher 在 Subscriber 上调用
receive(_:)
发送值; -
Publisher 在 Subscriber 上调用
receive(completion:)
来发送 comlpetion 作业。
Publisher 与 Subscriber 经过 Subscription 进行链接:
public protocol Subscription: Cancellable, CustomCombineIdentifierConvertible {
func request(_ demand: Subscribers.Demand)
}
Subscriber 调用 request(_:)
表示它希望接纳的值的数量,最多是无约束。
在 Subscriber 中,请注意 receive(_:)
回来一个 Demand
。 因而,即便 subscription.request(_:)
设置了 Subscriber 初始希望接纳的值的最大数量,咱们也能够在每次收到新值时调整该最大值。在 Subscriber.receive(_:)
中调整的最大值,是与之前的最大值累加的。 这意味着咱们能够在每次收到新值时,增加最大值,但不能进行削减。
自界说 Subscriber
收拾 Playground 代码,并增加以下代码:
import Combine
final class IntSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.max(3))
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received value: ", input)
return .none
}
func receive(completion: Subscribers.Completion<Never>) {
print("Received completion: ", completion)
}
}
let publisher = (1...5).publisher
咱们界说一个自界说 Subscriber:IntSubscriber
。指定此 Subscriber 的 Input
、Failure
,能够接纳 Int
类型的值,而且永远不会收到过错。接着完结所需的办法,receive(subscription:)
由 Publisher 调用,在该办法中调用 subscription
上的 request(_:)
办法,指定 Subscriber 希望接纳最多三个值。收到每个值后打印,回来 .none
,表示 Subscriber 不会调整自己对于值的数量多希望; .none
也等价于 .max(0)
。收到 completion 作业时,打印作业。
持续在 Playground 中增加以下代码:
let publisher = (1...5).publisher
let subscriber = IntSubscriber()
publisher.subscribe(subscriber)
咱们经过 range 的创立宣布 Int
类型值的 publisher
。接着创立一个与 Publisher 的 Output
和 Failure
类型相匹配的 Subscriber。终究为 Publisher subscribe
Subscriber。
运转 Playground,咱们将看到以下打印到操控台:
Received value: 1
Received value: 2
Received value: 3
咱们没有收到 completion 作业,这是由于咱们指定了 .max(3)
的最大数量。在 IntSubscriber
的 receive(_:)
中,尝试将 .none
更改为 .unlimited
,再次运转 Playground,这次咱们将看到操控台的输出包括一切值以及 completion 作业:
Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Received completion: finished
尝试将 .unlimited
更改为 .max(1)
并再次运转 Playground:
Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Received completion: finished
现在操控台打印的内容和 .unlimited
时相同,由于每次收到值时,咱们都指定要将最大值增加 1。
Future
之前咱们运用 Just
创立一个向 Subscriber 宣布单个值然后完结的 Publisher。Future
能够用于异步生成单个成果然后再完结。 收拾 Playground 并增加:
import Foundation
import Combine
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
func futureIncrement(
integer: Int,
afterDelay delay: TimeInterval
) -> Future<Int, Never> {
Future<Int, Never> { promise in
print("Original")
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
promise(.success(integer + 1))
}
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
使 Playground 能够获得异步履行的成果。
咱们创立了一个回来 Future<Int, Never>
的 futureIncrement
函数,它将宣布一个整数而且永远不会失利。在函数内部,这咱们创立了 future
,它 block 中供给了一个 promise
,咱们在完结异步操作后,供给值履行该 promise
,然后以在推迟 delay
时刻后,递增 integer
。
来看看 Future
的 Definition:
final public class Future<Output, Failure> : Publisher where Failure: Error {
public typealias Promise = (Result<Output, Failure>) -> Void
...
}
Promise
是一个闭包的别号,它接纳一个 Result
,其间包括由 Future
发布的单个值或过错。
回到 Playground ,在 futureIncrement
的界说之后增加以下代码:
var subscriptions = Set<AnyCancellable>()
let future = futureIncrement(integer: 1, afterDelay: 3)
future
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
在这里,运用咱们之前创立的函数创立一个 Future
,在三秒后递增咱们传递的整数 1。订阅并打印接纳到的值和完结作业,并将生成的 Subscription 存储在 subscriptions
中。
运转 Playground,咱们将在操控台看到:
Original
// ...三秒以后
2
finished
经过在 Playground 中增加以下代码来增加第二个 Subscription:
future
.sink(receiveCompletion: { print("Second: ", $0) },
receiveValue: { print("Second: ", $0) })
.store(in: &subscriptions)
从头运转 Playground,在指定的推迟之后,第二个 Subscription 接纳到相同的值。Future 不会从头履行 promise
,只有一个 Original
被打印,它共享或重放其输出:
Original
2
finished
Second: 2
Second: finished
咱们删去刚刚增加的两个 Subcription,只保留:
var subscriptions = Set<AnyCancellable>()
let future = futureIncrement(integer: 1, afterDelay: 3)
代码运转当即打印 Original。 产生这种状况是由于 Future
是贪婪的,一旦创立就会履行。它不像惯例 Publisher 那样是 lazy 的。
Subject
PassthroughSubject
咱们这部分将学习怎么创立自界说 Publisher —— Subject
。Subject
充当中间人,使非 Combine 的指令式代码能够向 Combine 的 Subscriber 发送值。将这个新示例增加到咱们收拾后的 Playground:
enum MyError: Error {
case test
}
final class StringSubscriber: Subscriber {
typealias Input = String
typealias Failure = MyError
func receive(subscription: Subscription) {
subscription.request(.max(2))
}
func receive(_ input: String) -> Subscribers.Demand {
print("Received value: ", input)
return input == "Combine" ? .max(1) : .none
}
func receive(completion: Subscribers.Completion<MyError>) {
print("Received completion: ", completion)
}
}
在上述代码中,咱们界说了 MyError
类型。接着界说了一个接纳 String
和 MyError
的自界说 Subscriber。该 Subscriber 依据收到的值调整希望,假如值为 Combine
则增加一的最大值。
持续增加代码:
let subject = PassthroughSubject<String, MyError>()
let subscriber = StringSubscriber()
subject.subscribe(subscriber)
let subscription = subject
.sink(
receiveCompletion: { completion in
print("Received completion (sink): ", completion)
},
receiveValue: { value in
print("Received value (sink): ", value)
})
咱们创立了一个 PassthroughSubject
实例 subject
。接着创立了一个自界说的 StringSubscriber
实例 subscriber
。为 subscriber
订阅 subject
。接着,咱们运用 sink
创立另一个 Subscription。
PassthroughSubject
使咱们能够按需发布值或许完结作业,它们将传递这些值和完结作业。与任何 Publisher 一样,咱们有必要提早声明它能够宣布的值和过错的类型,Subscriber 的输入和失利类型有必要和 PassthroughSubject
的宣布的值和过错的类型相匹配,才能成功订阅 PassthroughSubject
。
持续增加代码:
subject.send("Hello")
subject.send("Combine")
运用 subject
的 send
办法发送两个值。运转 Playground,咱们会看到的:
Received value: Hello
Received value (sink): Hello
Received value: Combine
Received value (sink): Combine
持续增加代码:
subscription.cancel()
subject.send("I am coming again.")
subject.send(completion: .finished)
subject.send("Is there anyone now?")
在这里咱们撤销了 subscription
,然后发送了另一个值,接着发送了完结时刻,终究再发送一个值。运转 Playground:
Received value (sink): Hello
Received value: Hello
Received value (sink): Combine
Received value: Combine
Received value: I am coming again.
Received completion: finished
由于咱们之前撤销了第二个 Subscriptor 的 Subscription,只有第一个 Subscriptor 会收到“I am coming again.”值。第一个 Subscriptor 没有收到“Is there anyone now?”值,由于它在 subject
发送值之前收到了 completion 作业。
在 subject.send(completion: .finished)
之前增加一行代码:
subject.send(completion: .failure(MyError.test))
运转 Playground,操控台会打印:
Received value (sink): Hello
Received value: Hello
Received value (sink): Combine
Received value: Combine
Received value: I am coming again.
Received completion: failure(Page_Contents.MyError.test)
第一个 Subscriptor 收到 .failure
成作业,没有收到后发送的 finished
完结作业。这表明一旦 Publisher 发送了一个 完结作业它就完结了。
CurrentValueSubject
运用 PassthroughSubject
传递值是将指令式代码连接到 Combine
的声明性国际的一种办法。 有时咱们还想在指令式代码中检查 Publisher 的当时值——咱们有另一个 subject:CurrentValueSubject
。
收拾 Playground 之前的代码,并将这个新示例增加到 playground 中:
var subscriptions = Set<AnyCancellable>()
let subject = CurrentValueSubject<Int, Never>(0)
subject
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
首要创立 Subscription set subscriptions
。接着创立类型为 Int
和 Never
的 CurrentValueSubject
,其初始值为 0。创立 subject
的 Subscription 并打印从 subject
接纳到的值。终究将 Subscription 存储在 subscriptions
set 中。
咱们有必要运用初始值初始化 CurrentValueSubject
;新 Subscriptor 当即获得该值或该 subject 发布的最新值。 运转 Playground:
0
接着,增加此代码以发送两个新值:
subject.send(1)
subject.send(2)
运转 Playground,操控台将输出:
0
1
2
与 PassthroughSubject
不同,咱们能够随时向 CurrentValueSubject
问询其 value
。 增加以下代码以打印出 subject
的当时值,持续增加代码并产看操控台输出:
print(subject.value)
0
1
2
2
除了在 CurrentValueSubject
上调用 send(_:)
发送新值, 另一种办法是为其 value 特点分配一个新值。 增加此代码:
subject.value = 3
print(subject.value)
运转 Playground,咱们会看到 2 和 3 别离打印了两次——一次由 Subscriber 打印,另一次经过 print
打印。
接下来,创立一个对 CurrentValueSubject
的新 Subscription:
subject
.sink(receiveValue: { print("Second subscription: ", $0) })
.store(in: &subscriptions)
咱们在上文了解到,在 subscriptions
set 开释时,会自动撤销增加到其间的 Subscription,咱们能够运用 print()
Operator,它将一切作业记录到操控台,修正两个 Subscription 代码,增加 print()
和完结作业:
// ...
subject
.print()
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
// ...
subject
.print()
.sink(receiveValue: { print("Second subscription: ", $0) })
.store(in: &subscriptions)
subject.send(completion: .finished)
再次运转 Playground,咱们将看到整个示例的以下输出:
receive subscription: (CurrentValueSubject)
request unlimited
receive value: (0)
0
receive value: (1)
1
receive value: (2)
2
2
receive value: (3)
3
3
receive subscription: (CurrentValueSubject)
request unlimited
receive value: (3)
Second subscription: 3
receive finished
receive finished
动态调整 demand
咱们之前了解到,在 Subscriber.receive(_:)
中调整 demand 是累加的。 咱们能够在更详细的示例中仔细研讨它是怎么作业的。 收拾 Playground 并增加新示例:
final class IntSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.max(2))
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received value", input)
switch input {
case 1:
return .max(2)
case 3:
return .max(1)
default:
return .none
}
}
func receive(completion: Subscribers.Completion<Never>) {
print("Received completion", completion)
}
}
let subscriber = IntSubscriber()
let subject = PassthroughSubject<Int, Never>()
subject.subscribe(subscriber)
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
subject.send(5)
subject.send(6)
subject.send(6)
大部分代码与之前的示例相似,咱们将专心于 receive(_:)
办法:
-
收到值 1,新的最大值为 4(2 + 2);
-
收到值 3,新的最大值为 5(4 + 1);
-
最大值维持 5。
运转 Playground,咱们将看到以下内容:
Received value 1
Received value 2
Received value 3
Received value 4
Received value 5
正如预期的那样,只打印了五个值,没有打印出第六个值。
类型擦除
有时咱们希望让 Subscriber 订阅来自 Publisher 的作业,而约束拜访有关该 Publisher 的其他信息。收拾并增加新的代码在 Playground 中:
var subscriptions = Set<AnyCancellable>()
let subject = PassthroughSubject<Int, Never>()
let publisher = subject.eraseToAnyPublisher()
publisher
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
subject.send(0)
咱们创立一个 PassthroughSubject
,从 subject
创立一个类型擦除的 Publisher publisher
。订阅类型擦除的 publisher
。经过 subject
发送新值。
publisher
的的类型为 AnyPublisher<Int, Never>
。AnyPublisher
是契合 Publisher
协议的类型擦除结构。类型擦除答应咱们躲藏或许不想向 Subscriber 或下游 Publisher 揭露的 Publisher 的信息。
其实 AnyCancellable
也是一个契合 Cancellable
的类型擦除类,它答应调用者撤销 Subscription,而无需拜访底层 的 Subscription 来履行其他操作。eraseToAnyPublisher()
Operator 将实际的 Publisher 包装在 AnyPublisher
的实例中,躲藏 Publisher 是 PassthroughSubject
类的现实。
AnyPublisher
没有 send(_:)
办法,因而咱们不能直接向该发布者增加新值。当咱们想要运用一对 Public 和 Private 特点时,答应这些特点的一切者在 Private Publisher 上发送值,外部调用者只订阅但不能发送值。
假如咱们将上述代码中的 subject.send(0)
替换为 publisher.send(0)
,代码将提示过错:
Value of type 'AnyPublisher<Int, Never>' has no member 'send'
桥接 Combine Poblisher 到 async/await
在 iOS 15 和 macOS 12 中,Swift 5.5 中的 Combine 结构新增了两个很棒的功用,协助咱们轻松地将 Combine 与 Swift 中的 async/await 语法结合运用。
收拾并增加新的代码在 Playground 中:
import Foundation
import Combine
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let subject = CurrentValueSubject<Int, Never>(0)
Task {
for await element in subject.values {
print("Element: \(element)")
}
print("Completed.")
}
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .finished)
在此示例中,咱们运用 CurrentValueSubject
,相同,API 可用于任何契合 Publisher
的类型。
Task 创立一个新的异步任务,闭包内的代码将异步运转。咱们运用 for 循环来迭代这些元素的异步序列,一旦发布者完结,无论是成功仍是失利,循环都会结束。
再次运转 Playground 代码,咱们将看到以下输出:
Element: 0
Element: 1
Element: 2
Element: 3
Completed.
内容参考
-
Combine | Apple Developer Documentation;
-
来自 Kodeco 的书本《Combine: Asynchronous Programming with Swift》;
-
对上述 Kodeco 书本的汉语自译版 《Combine: Asynchronous Programming with Swift》收拾。