This article will introduce the concepts and best practices of Combine from the basic idea of reactive programming and step by step, hoping to help more people to get started and practice reactive programming successfully.

## Reactive Programming

In computing, reactive programming is a declarative programming paradigm oriented to data flow and change propagation. –wiki

## Declarative Programming

Declarative and imperative programming are common programming paradigms. In imperative programming, the developer makes the computer execute the program by combining statements such as operations, loops, and conditions. Declarative is the opposite of imperative, in that if an imperative is like telling the computer how to do, a declarative is telling the computer what to do. in fact, we have all been exposed to declarative programming, but we don’t realize it when we code. DSLs and functional programming of all kinds fall under the category of declarative programming.

For example, suppose we want to get all the odd numbers in a shape-shifting array. Following imperative logic, we need to break down the process into step-by-step statements that

1. iterate through all the elements of the array.
2. determine if it is an odd number.
3. If it is, add it to the result. Continue the traversal.
 ``````1 2 3 4 5 6 `````` ``````var results = [Int]() for num in values { if num %2 != 0 { results.append(num) } } ``````

If we go by declarative programming, the idea might be to “filter out all odd numbers”, and the corresponding code would be very intuitive.

 ``````1 `````` ``````var results = values.filter { \$0 % 2 != 0 } ``````

There is a clear distinction between the two types of programming mentioned above.

• Instructional programming: describes the process (How) and the computer executes it directly and gets the result.
• Declarative programming: Describes the result (What) and lets the computer organize the specific process for us and finally gets the described result.

## Data flow oriented and change propagation

In layman’s terms, data-flow oriented and change propagation is reactive to the flow of events that occur in the future.

1. Event Posting: An operation posts an event `A`, and the event `A` can carry an optional data `B`.
2. Operation morphing: Event `A` and data `B` are changed by one or more operations, resulting in event `A'` and data `B'`.
3. Subscription Usage: On the consumer side, one or more subscribers consume the processed `A'` and `B'` and further drive other parts (e.g. UI ).

In this flow, a myriad of events make up the event stream, and subscribers are constantly receiving and responding to new events.

At this point, we have an initial understanding of the definition of reactive programming, i.e., responding to future occurrences of event streams in a declarative manner. In practice coding, many good three-party libraries further abstract this mechanism and provide developers with interfaces with varying functionality. In iOS development, there are three dominant “schools” of reactiveness.

### Reactive genre

• ReactiveX：RxSwift
• Reactive Streams：Combine
• Reactive*：ReactiveCocoa / ReactiveSwift ／ReactiveObjc

These three schools are ReactiveX, Reactive Streams, and Reactive*. ReactiveX is described in more detail next, and Reactive Stream aims to define a standard for non-blocking asynchronous event stream processing, which Combine has chosen as the specification for its implementation. Reactive*, represented by ReactiveCocoa, was very popular in the Objective-C era, but with the rise of Swift, more developers chose RxSwift or Combine, causing Reactive* to decline in popularity overall.

### ReactiveX (Reactive Extension)

ReactiveX was originally a reactive extension implemented by Microsoft on . Its interfaces are not intuitively named, such as Observable and Observer, and the strength of ReactiveX is the innovative incorporation of many functional programming concepts, making the entire event stream very flexible in its morphing. This easy-to-use and powerful concept was quickly adopted by developers of all languages, so ReactiveX is very popular in many languages with corresponding versions (e.g. RxJS, RxJava, RxSwift), and Resso’s Android team is using RxJava heavily.

## Why Combine

Combine is an RxSwift-like asynchronous event processing framework coming from Apple in 2019.

Combine provides a set of declarative Swift APIs to handle values that change over time. These values can represent user interface events, network responses, scheduled events, or many other types of asynchronous data.

The Resso iOS team also briefly tried RxSwift, but after a closer look at Combine, we found that Combine outperformed RxSwift in terms of performance, ease of debugging, and the special advantages of a built-in framework and SwiftUI official configuration, and were attracted by its many advantages to switch to Combine.

Combine has a number of advantages over RxSwift.

• Apple product
• Built into the system, no impact on App package size
• Better performance
• Debug is easier
• SwiftUI official

#### Performance Benefits

Combine has more than 30% performance improvement over RxSwift for all operations.

Reference: Combine vs. RxSwift Performance Benchmark Test Suite

#### Debug Benefits

Since Combine is a library, with the `Show stack frames without debug symbols and between libraries` option turned on in Xcode, the number of invalid stacks can be significantly reduced, improving Debug efficiency.

 ``````1 2 3 4 5 6 `````` ``````// 在 GlobalQueue 中接受并答应出数组中的值 [1, 2, 3, 4].publisher .receive(on: DispatchQueue.global()) .sink { value in print(value) } ``````

### Combine interface

As mentioned above, Combine’s interface is based on the Reactive Streams Spec implementation, where concepts such as `Publisher`, `Subscriber`, and `Subscription` are already defined, with some fine-tuning by Apple.

Specifically at the interface level, the Combine API is more similar to the RxSwift API. The missing interfaces in Combine can be replaced by other existing interfaces, and a few operators are available in open source third-party implementations, so there is no impact on the production environment.

### OpenCombine

If you are a careful reader, you may have noticed the presence of OpenCombine in the Debug Advantage diagram, which is great, but has one fatal drawback: it requires a minimum system version of iOS 13, which is not available for many apps that require multiple system versions.

The OpenCombine community has been kind enough to implement an open source implementation of Combine that only requires iOS 9.0: OpenCombine, which has been tested internally to be on par with Combine in terms of performance. The cost of migrating from OpenCombine to Combine is also very low, with only a simple text replacement job. OpenCombine is also used by Resso, Cut Image, Waking Image, and Lark in our company.

## Combine Basic Concepts

As mentioned above, the concept of Combine is based on Reactive Streams. three key concepts in reactive programming, event publication/operation transformation/subscription usage, correspond to `Publisher` , `Operator` and `Subscriber` in Combine.

In the simplified model, there is first a `Publisher` that is transformed by an `Operater` and then consumed by a `Subscriber`. In actual coding, the source of `Operator` may be a plural `Publisher`, and `Operator` may be subscribed by multiple `Publishers`, usually forming a very complex graph.

### Publisher

 ``````1 `````` ``````Publisher ``````

`Publisher` is the source of Event generation. Events are a very important concept in Combine and can be divided into two categories, one carrying a value (`Value`) and the other marking the end (`Completion`). The end can be either a normal completion (`Finished`) or a failure (`Failure`).

 ``````1 2 3 4 5 `````` ``````Events： - Value：Output - Completion - Finished - Failure(Error) ``````

Typically, a `Publisher` can generate `N` events before ending. Note that once a `Publisher` has issued a `Completion` (which can be either a normal completion or a failure), the entire subscription will end and no events can be issued after that.

Apple provides Combine extensions to the Publisher for many common classes in the official base library, such as Timer, NotificationCenter, Array, URLSession, KVO, and more. Using these extensions we can quickly combine a Publisher, such as

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 `````` ``````// `cancellable` 是用于取消订阅的 token，下文会详细介绍 cancellable = URLSession.shared // 生成一个 https://example.com 请求的 Publisher .dataTaskPublisher(for: URL(string: "https://example.com")!) // 将请求结果中的 Data 转换为字符串，并忽略掉空结果，下面会详细介绍 compactMap .compactMap { String(data: \$0.data, encoding: .utf8) } // 在主线程接受后续的事件 （上面的 compactMap 发生在 URLSession 的线程中） .receive(on: RunLoop.main) // 对最终的结果（请求结果对应的字符串）进行消费 .sink { _ in // } receiveValue: { resultString in self.textView.text = resultString } ``````

In addition, there are some special `Publisher`s that are also very useful.

• `Future`: only generates one event, either success or failure, suitable for most simple callback scenarios
• `Just`: a simple wrapper around a value, like `Just(1)`
• `@Published` : described in more detail below In most cases, using these special `Publisher`s and the `Subject`s described below allows flexible combinations of event sources to meet your needs.

### Subscriber

 ``````1 `````` ``````Subscriber ``````

A `Subsriber` is defined as a subscriber to an event and corresponds to a `Publisher`, where the `Output` in the `Publisher` corresponds to the `Input` of the `Subscriber`. Commonly used `Subscriber`s are `Sink` and `Assign`.

`Sink` is used to subscribe directly to the event stream, and can handle `Value` and `completion` separately.

The word `Sink` can be very confusing at first sight. The term can be derived from the sink in a network stream, which we can also understand as The stream goes down the sink.

 ``````1 2 3 4 5 6 7 8 `````` ``````// 从数组生成一个 Publisher cancellable = [1, 2, 3, 4, 5].publisher .sink { completion in // 处理事件流结束 } receiveValue: { value in // 打印会每个值，会依次打印出 1, 2, 3, 4, 5 print(value) } ``````

`Assign` is a special version of `Sink` that supports direct assignment via `KeyPath`.

 ``````1 2 3 4 5 `````` ``````let textLabel = UILabel() cancellable = [1, 2, 3].publisher // 将 数字 转换为 字符串，并忽略掉 nil ，下面会详细介绍这个 Operator .compactMap { String(\$0) } .assign(to: \.text, on: textLabel) ``````

Note that assigning `self` with `assign` may result in an implicit circular reference, in which case you need to manually assign `sink` with `weak self` instead.

#### Cancellable & AnyCancellable

Careful readers may have noticed the presence of a `cancellable` above. Each subscription generates an `AnyCancellable` object, which is used to control the lifecycle of the subscription. Through this object, we can cancel the subscription. When this object is released, the subscription will also be cancelled.

 ``````1 2 `````` ``````// 取消订阅 cancellable.cancel() ``````

Note that for each subscription we need to hold this `cancellable`, otherwise the whole subscription will be cancelled and ended immediately.

### Subscriptions

The connection between `Publisher` and `Subscriber` is made via `Subscription`. Understanding the entire subscription process can be very helpful when using Combine in depth.

Combine’s subscription process is actually a pull model.

1. `Subscriber` initiates a subscription, telling `Publisher` that I need a subscription.
2. `Publisher` returns a subscription entity (`Subscription`).
3. `Subscriber` requests a fixed amount (`Demand`) of data through this `Subscription`.
4. `Publisher` returns events based on `Demand`. After a single `Demand` is published, if the `Subscriber` continues to request events, the `Publisher` will continue to publish.
5. Continue the publishing process.
6. When all the events requested by the `Subscriber` have been published, the `Publisher` sends a `Completion`.

### Subject

 ``````1 `````` ``````Subject ``````

`Subject` is a special class of `Publisher` that can be used to manually inject new events into the event stream via method calls such as `send()`.

 ``````1 2 3 `````` ``````private let isPlayingPodcastSubject = CurrentValueSubject(false) // 向 isPlayingPodcastPublisher 注入一个新的事件，它的值是 true isPlayingPodcastSubject.send(true) ``````

Combine provides two common `Subject`s: `PassthroughSubject` and `CurrentValueSubject`.

• `PassthroughSubject`: Passes through events and does not hold the latest `Output`.
• `CurrentValueSubject`: holds the latest `Output` in addition to the passed events.

#### @Published

For those who are new to Combine, there is no greater problem than finding an event source that you can use directly. Combine provides a Property Wrapper `@Pubilshed` to quickly wrap a variable to get a `Publisher`.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 `````` ``````// 声明变量 class Alarm { @Published public var countDown = 0 } let alarm = Alarm() // 订阅变化 let cancellable = alarm.\$countDown // Published.Publisher .sink { print(\$0) } // 修改 countDown，上面 sink 的闭包会触发 alarm.countDown += 1 ``````

What is interesting above is that `\$countDown` accesses a `Publisher`, which is actually syntactic sugar, `\$` accesses what is actually the `projectedValue` of `countDown`, which is the corresponding `Publisher`.

 ``````1 2 3 4 5 6 7 `````` ``````@propertyWrapper public struct Published { // ... /// The property for which this instance exposes a publisher /// /// The ``Published/projectedValue` is the property accessed with the `\$` operator public var projectedValue: Published.Publisher { mutating get set } } ``````

@Published is great for encapsulating events within a module, type-erasing them and then making them available externally for subscription consumption.

In practice, for existing code logic, @Published can be used to quickly give properties the ability to be Publisher without changing other code. For new code, @Published is also a good choice if no errors occur and the current Value needs to be used, but otherwise consider using PassthroughSubject or CurrentValueSubject on an as-needed basis.

### Operator

Combine comes with a very rich set of Operators, and we will introduce some of them.

#### map, filter, reduce

Students familiar with functional programming should be familiar with these Operators. Their effects are very similar to those on arrays, except this time in an asynchronous event stream.

For example, for `map`, he transforms the values in each event.

 ``````1 2 3 4 5 6 `````` ``````[1, 2, 3].publisher .map { \$0 * 10 } .sink { value in // 将会答应出 10, 20, 30 print(value) } ``````

`filter` is similar, filtering each event with the conditions in the closure. `reduce`, on the other hand, will compute the value of each event and finally pass the result of the computation downstream.

#### compactMap

For event streams whose Value is `Optional`, you can use `compactMap` to get a Publisher whose Value is a non-empty type.

 ``````1 2 3 4 5 `````` ``````// Publiser -> Publisher cancellable = [1, nil, 2, 3].publisher .compactMap { \$0 } .map { \$0 * 10 } .sink { print(\$0) } ``````

#### flatMap

`flatMap` is a special operator that converts each of the events into an event stream and merges them together. For example, when the user enters text in the search box, we can subscribe to the text changes and generate the corresponding search request Publisher for each text and aggregate all the Publisher’s events together for consumption.

Other common Operators are `zip` , `combineLatest` and so on.

### Type Erasure

The `Publisher` in Combine gets a multi-layer generic nested type after various `Operator` transformations.

 ``````1 2 3 4 `````` ``````URLSession.shared.dataTaskPublisher(for: URL(string: "https://resso.com")!) .map { \$0.data } .decode(type: String.self, decoder: JSONDecoder()) // 这个 publisher 的类型是 Publishers.Decode, String, JSONDecoder> ``````

This does not pose any problem if the subscription is consumed as soon as the `Publisher` is created and deformed. However, once we need to make this `Publisher` available for external use, complex types can expose too many internal implementation details and also make the function/variable definition very bloated. Combine provides a special operator `erasedToAnyPublisher` that allows us to erase the specific type.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 `````` ``````// 生成一个类型擦除后的请求。函数的返回值更简洁 func requestRessoAPI() -> AnyPublisher { let request = URLSession.shared.dataTaskPublisher(for: URL(string: "https://resso.com")!) .map { \$0.data } .decode(type: String.self, decoder: JSONDecoder()) // Publishers.Decode, String, JSONDecoder> // to // AnyPublisher return request.eraseToAnyPublisher() } // 在模块外，不用关心 `requestRessoAPI()` 返回的具体类型，直接进行消费 cancellable = requestRessoAPI().sink { _ in } receiveValue: { print(\$0) } ``````

With type erasure, the final exposure to the outside world is a simple `AnyPublisher<String, Error>`.

### Debugging

reactive programming is very easy to write, but debugging is not as pleasant. In response, Combine also provides several Operators to help developers debug.

#### Debug Operator

`print` and `handleEvents`

`print` prints out the entire subscription process from start to finish with all the changes and values, e.g.

 ``````1 2 3 4 5 `````` ``````cancellable = [1, 2, 3].publisher .receive(on: DispatchQueue.global()) // 使用 `Array Publisher` 作为所有打印内容的前缀 .print ( "Array Publisher") .sink { _ in } ``````

You can get:

 ``````1 2 3 4 5 6 7 `````` ``````Array Publisher: receive subscription: (ReceiveOn) Array Publisher: request unlimited Array Publisher: receive cancel Array Publisher: receive value: (1) Array Publisher: receive value: (2) Array Publisher: receive value: (3) Array Publisher: receive finished ``````

In some cases, we are only interested in some of the events in all the changes, and this can be done by printing some of the events with `handleEvents`. Similarly there is `breakpoint`, which can trigger a breakpoint when an event occurs.

#### Drawing method

When you get to the point where you’ve run out of ideas, it’s good to use images to clarify your thinking. For a single Operator, you can find the corresponding Operator in RxMarble to check if you understand it correctly. For complex subscriptions, you can draw a diagram to confirm that the event flow is being delivered as expected.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 `````` ``````let greetings = PassthroughSubject() let names = PassthroughSubject() let years = PassthroughSubject() // CombineLatest 会选用两个事件流中最新的值生成新的事件流 let greetingNames = Publishers.CombineLatest(greetings, names) .map {"\(\$1) \(\$0)" } let wholeSentence = Publishers.CombineLatest(greetingNames, years) .map { ")(\$0), \(\$1)" } .sink { print(\$0) } greetings.send("Hello") names.send("Combine") years.send(2022) ``````

### Common Errors

#### Just and Future that start immediately

For most `Publishers`, they start producing events only after subscription, but there are some exceptions. `Just` and `Future` execute closures to produce events immediately after initialization is complete, which may allow some time-consuming operations to start earlier than expected, and may allow the first subscription to miss some events that start too early.

 ``````1 2 3 4 `````` ``````func makeMyPublisher () -> AnyPublisher { Just(calculateTimeConsumingResult()) .eraseToAnyPublisher() } ``````

A possible solution is to wrap a layer of `Defferred` outside such `Publisher` and let it start executing the internal closure after receiving the subscription.

 ``````1 2 3 4 5 `````` ``````func makeMyFuture2( ) -> AnyPublisher { Deferred { return Just(calculateTimeConsumingResult()) }.eraseToAnyPublisher() } ``````

#### An error occurred causing the Subscription to end unexpectedly

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 `````` ``````func requestingAPI() -> AnyPublisher { return URLSession.shared .dataTaskPublisher(for: URL(string: "https://resso.com")!) .map { \$0.data } .decode(type: String.self, decoder: JSONDecoder()) .eraseToAnyPublisher() } cancellable = NotificationCenter.default .publisher(for: UserCenter.userStateChanged) .flatMap({ _ in return requestingAPI() }) .sink { completion in } receiveValue: { value in textLabel.text = value } ``````

The above code converts a notification of user status into a network request and updates the result of the request to a Label. Note that if an error occurs in a network request, the entire subscription will be terminated and subsequent new notifications will not be converted into requests.

 ``````1 2 3 4 5 6 7 8 `````` ``````cancellable = NotificationCenter.default .publisher(for: UserCenter.userStateChanged) .flatMap { value in return requestingAPI().materialize() } .sink { text in titleLabel.text = text } ``````

There are many ways to solve this problem, the above uses `materialize` to convert events from `Publisher<Output, MyError>` to `Publisher<Event<Output, MyError>, Never>` to avoid errors.

Combine does not officially implement materialize, CombineExt provides an open source implementation.

### Combine In Resso

Resso uses Combine in many scenarios, the most classic example is the logic of getting multiple attributes in the sound effect function. The sound effect needs to use the album cover, the album theme color, and the song’s corresponding effects configuration to drive the sound effect playback. These three properties need to be fetched using three network requests, and if you use the classic iOS closure callbacks to write this part of the logic, you’re nesting three closures and getting stuck in callback hell, not to mention the possibility of missing the wrong branch.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 `````` ``````func startEffectNormal() { // 1. 获取歌曲封面 WebImageManager.shared.requestImage(trackCoverURL) { result in switch result { case .success(let image): // 2. 获取特效配置 fetchVisualEffectConfig(for: trackID) { result in switch result { case .success(let path): // 3. 获取封面主题色 fetchAlbumColor(trackID: trackID) { result in switch result { case .success(let albumColor): self.startEffect(coverImage: coverImage, effectConfig: effectConfig, coverColor: coverColor) case .failure: // 处理获取封面颜色错误 break } } case .failure(let error): // 处理获取特效配置错误 break } } case .failure(let error): // 处理下载图片错误 break } } } ``````

Using Combine, we can wrap the three requests into a single `Publisher` and use the three results together with `combineLatest`.

 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 `````` ``````func startEffect() { // 获取歌曲封面的 Publisher cancellable = fetchTrackCoverImagePublisher(for: trackCoverURL) // 并与 获取特效配置的 Publisher 和 获取专辑主题色的 Publisher 中的最新结果组成新的 Publisher .combineLatest(fetchVisualEffectPathPublisher(for: trackID), fetchAlbumColorPublisher(trackID: trackID)) // 对最终的结果进行使用 .sink { completion in if case .failure(let error) = completion { // 对错误进行处理 } } receiveValue: { (coverImage, effectConfig, coverColor) in self.startEffect(coverImage: coverImage, effectConfig: effectConfig, coverColor: coverColor) } } ``````

Such an implementation brings a number of benefits.

1. more compact code structure and better readability
2. more focused error handling, less likely to be missed
3. better maintainability, if you need a new request, just continue to combine new Publisher

In addition, Resso has also implemented Combine extensions to its own web library to make it easier for more people to start using Combine.

 ``````1 2 3 4 5 `````` ``````func fetchSomeResource() -> RestfulClient.DataTaskPublisher{ let request = SomeRequest() return RestfulClient(request: request) .dataTaskPublisher } ``````

## Summary

In a nutshell, the core of reactive programming is responding to future streams of events in a declarative way. In everyday development, using reactive programming wisely can simplify code logic dramatically, but abusing it in inappropriate scenarios (or even all scenarios) can leave colleagues 🤬. The common multiple nested callbacks and custom notifications are perfect for cut-and-dried scenarios.

Combine is a concrete implementation of reactive programming, and its built-in system and excellent implementation give it many advantages over other reactive frameworks. Learning and mastering Combine is a great way to practice reactive programming and has many benefits for everyday development.