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 都能够宣布这三种类型的多个作业:

  1. 该 Publisher 的 Output 类型的值;

  2. 表示成功的 completion;

  3. 带有该 Publisher 的 Failure 类型的 Error 的 completion。

Publisher 能够宣布零个或多个 Output 类型的值,假如它完结了,无论是成功仍是失利,后续它都不会宣布任何其他作业。

以发布 Int 值的 Publisher 在时刻轴上的可视化效果为例:

Combine | (I) Hello, Combine!

蓝色框表示在时刻线上给定时刻 Publisher 宣布的值。图表右竖线表示 stream 的成功完结。三种类十分普遍,代表了咱们 App 中的任何类型的动态数据。

Publisher 协议有两种类型是通用的,正如前文提到:

  • Publisher.Output 是 Publisher 输出的值的类型。 一个 Int Publisher 永远不能直接宣布 String 类型值。
  • Publisher.Failure 是 Publisher 在失利时能够抛出的过错类型。 假如 Publisher 永远不会失利,咱们能够指定为 Never 类型来。

当咱们订阅某个 Publisher 时,会希望从中获得什么值以及或许会因哪些过错而失利,即 Publisher.OutputPublisher.Failure

操作符(Operator)根底概念

Operator 是在 Publisher 协议上声明的办法,它们回来相同的或新的 Publisher。咱们能够一个接一个地调用操作符,然后有效地将它们链接在一起。这些操作符是高度解耦、可组合的,所以它们能够组合起来在单个订阅(Subscription) 中完结复杂逻辑。操作符总的输入和输出,一般称为 上游(Upstream)下游(Downstream)。但需要注意,假如一个 Operator 的输出与下一个 Operator 的输入类型不匹配,则不能组合在一起。

Combine | (I) Hello, Combine!

操作符专心于处理从前一个操作符接纳到的数据,并将其输出供给给链中的下一个操作符。咱们能够以清晰的办法,来界说每个异步的、笼统的作业的次序,以及有清晰的输入、输出类型和过错处理。

订阅者(Subscriber)根底概念

到达订阅链的结尾,每个订阅都以一个 Subscriber 结束。Subscriber 一般对输出的值或作业做些什么。

Combine | (I) Hello, Combine!

目前,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 之类的代码:

Combine | (I) Hello, Combine!

此外,咱们不需要专门办理 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 来操控视图:

Combine | (I) Hello, Combine!

发布者(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 来监听 centermyNotification 类型的 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 类,valuedidSet 后,将打印当时 value

创立 MyObject 类的实例 object。从 String 数组创立 publisher,订阅该 publisher,将收到的值分配给 objectvalue 特点。运转 Playground,操控台将终究展示:

Hello
Combine!

assign(to:on:) 在处理 UIKit 或 AppKit 结构的 App 时特别有用,咱们能够将值直接分配给 labelbutton 等 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 并将它宣布的每个值 assignobjectvalue 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 之间的相互作用,以下是一个简略概述:

Combine | (I) Hello, Combine!

  1. Subscriber 订阅 Publisher;

  2. Publisher 创立 Subscription 并将其供给给 Subscriber;

  3. Subscriber 请求值;

  4. Publisher 发送对应数量的值;

  5. 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
}
  1. Output 是 Publisher 生成的值的类型;

  2. Failure 是 Publisher 或许产生的过错类型,假如 Publisher 保证不会产生过错,则为 Never

  3. 先看 extension 中的办法,Subscription 时,Subscriber 在 Publisher 上调用 subscribe(_:)

  4. 回过头看 receive(subscriber:) ,刚刚的 subscribe(_:) 的完结将调用 receive(subscriber:) ,将 Subscriber 附加到 Publisher 上,即创立 Subscription。

Subscriber 有必要匹配 Publisher 的 OutputFailure 才能创立订阅。咱们接着看看 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>)
}
  1. Input 是 Subscriber 能够接纳的值的类型;

  2. Failure 是 Subscriber 能够接纳的过错类型; 假如 Subscriber 不会收到过错则为 Never

  3. Publisher 在 Subscriber 上调用 receive(subscription:) 来给 Subscriber 回来 Subscription;

  4. Publisher 在 Subscriber 上调用 receive(_:)发送值;

  5. 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 的 InputFailure,能够接纳 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 的 OutputFailure 类型相匹配的 Subscriber。终究为 Publisher subscribe Subscriber。

运转 Playground,咱们将看到以下打印到操控台:

Received value:  1
Received value:  2
Received value:  3

咱们没有收到 completion 作业,这是由于咱们指定了 .max(3) 的最大数量。在 IntSubscriberreceive(_:) 中,尝试将 .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 —— SubjectSubject 充当中间人,使非 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 类型。接着界说了一个接纳 StringMyError 的自界说 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")

运用 subjectsend 办法发送两个值。运转 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。接着创立类型为 IntNeverCurrentValueSubject,其初始值为 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. 收到值 1,新的最大值为 4(2 + 2);

  2. 收到值 3,新的最大值为 5(4 + 1);

  3. 最大值维持 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》收拾。