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
- 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 eventA
can carry an optional dataB
. - Operation morphing: Event
A
and dataB
are changed by one or more operations, resulting in eventA'
and dataB'
. - Subscription Usage: On the consumer side, one or more subscribers consume the processed
A'
andB'
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.
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
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.
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
|
|
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
).
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
|
|
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 scenariosJust
: a simple wrapper around a value, likeJust(1)
@Published
: described in more detail below In most cases, using these specialPublisher
s and theSubject
s described below allows flexible combinations of event sources to meet your needs.
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.
Assign
is a special version of Sink
that supports direct assignment via KeyPath
.
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.
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.
Subscriber
initiates a subscription, tellingPublisher
that I need a subscription.Publisher
returns a subscription entity (Subscription
).Subscriber
requests a fixed amount (Demand
) of data through thisSubscription
.Publisher
returns events based onDemand
. After a singleDemand
is published, if theSubscriber
continues to request events, thePublisher
will continue to publish.- Continue the publishing process.
- When all the events requested by the
Subscriber
have been published, thePublisher
sends aCompletion
.
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()
.
Combine provides two common Subject
s: PassthroughSubject
and CurrentValueSubject
.
PassthroughSubject
: Passes through events and does not hold the latestOutput
.CurrentValueSubject
: holds the latestOutput
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
.
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
.
@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.
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.
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.
Practical advice
Type Erasure
The Publisher
in Combine gets a multi-layer generic nested type after various Operator
transformations.
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 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.
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.
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.
|
|
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.
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 combineLatest
.
|
|
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.
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.