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.
In computing, reactive programming is a declarative programming paradigm oriented to data flow and change propagation. –wiki
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
- iterate through all the elements of the array.
- determine if it is an odd number.
- If it is, add it to the result. Continue the traversal.
If we go by declarative programming, the idea might be to “filter out all odd numbers”, and the corresponding code would be very intuitive.
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.
- Event Posting: An operation posts an event
A, and the event
Acan carry an optional data
- Operation morphing: Event
Bare changed by one or more operations, resulting in event
- Subscription Usage: On the consumer side, one or more subscribers consume the processed
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 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.
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.
Advantages of 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
Combine has more than 30% performance improvement over RxSwift for all operations.
Reference: Combine vs. RxSwift Performance Benchmark Test Suite
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.
As mentioned above, Combine’s interface is based on the Reactive Streams Spec implementation, where concepts such as
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.
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
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
Operator may be subscribed by multiple
Publishers, usually forming a very complex graph.
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 (
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
In addition, there are some special
Publishers 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
@Published: described in more detail below In most cases, using these special
Publishers and the
Subjects described below allows flexible combinations of event sources to meet your needs.
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
Sink is used to subscribe directly to the event stream, and can handle
Sinkcan 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.
Assign is a special version of
Sink that supports direct assignment via
Note that assigning
assign may result in an implicit circular reference, in which case you need to manually assign
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.
Note that for each subscription we need to hold this
cancellable, otherwise the whole subscription will be cancelled and ended immediately.
The connection between
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.
Subscriberinitiates a subscription, telling
Publisherthat I need a subscription.
Publisherreturns a subscription entity (
Subscriberrequests a fixed amount (
Demand) of data through this
Publisherreturns events based on
Demand. After a single
Demandis published, if the
Subscribercontinues to request events, the
Publisherwill continue to publish.
- Continue the publishing process.
- When all the events requested by the
Subscriberhave been published, the
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
Combine provides two common
PassthroughSubject: Passes through events and does not hold the latest
CurrentValueSubject: holds the latest
Outputin addition to the passed events.
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
What is interesting above is that
$countDown accesses a
Publisher, which is actually syntactic sugar,
$ accesses what is actually the
countDown, which is the corresponding
@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.
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.
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.
For event streams whose Value is
Optional, you can use
compactMap to get a Publisher whose Value is a non-empty type.
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
combineLatest and so on.
Publisher in Combine gets a multi-layer generic nested type after various
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.
With type erasure, the final exposure to the outside world is a simple
reactive programming is very easy to write, but debugging is not as pleasant. In response, Combine also provides several Operators to help developers debug.
You can get:
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.
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.
Just and Future that start immediately
Publishers, they start producing events only after subscription, but there are some exceptions.
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.
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.
An error occurred causing the Subscription to end unexpectedly
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.
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.
Using Combine, we can wrap the three requests into a single
Publisher and use the three results together with
Such an implementation brings a number of benefits.
- more compact code structure and better readability
- more focused error handling, less likely to be missed
- 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.
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.