A year and a half ago, I
sang the praises of RxSwift . It took me a while to figure it out, but when that happened, there was no turning back. Now I had the best hammer in the world, and damn me if everything around me didn't seem like a nail.
Apple introduced the
Combine framework at
WWDC Summer Conference. At first glance, it looks like a slightly better version of RxSwift. Before I can explain what I like about it and what not, we need to understand what problem Combine is designed to solve.
Reactive programming? So what?
The ReactiveX community - of which the RxSwift community is a part - explains its essence as follows:
API for asynchronous programming with observable threads.
And further:
ReactiveX is a combination of the best ideas from the Observer and Iterator design patterns, as well as functional programming.
Well ... well.
And what does this
really mean?
The basics
To really understand the essence of reactive programming, I find it useful to understand how we came to it. In this article, I will describe how you can look at existing types in any modern OOP language, twist them, and then come to reactive programming.
In this article, we will quickly delve into the jungle, which is not
absolutely necessary for understanding reactive programming.
However, I consider this a curious academic exercise, especially in terms of how strongly typed languages can lead us to new discoveries.
So wait for my next posts if you are interested in new details.
Enumerable
The “
reactive programming ” known to me originated in the language in which I once wrote - C #. The premise itself is quite simple:
What if, instead of extracting values from enumerable, they will send you the values themselves?
This idea, “push instead of pull,” was best
described by Brian Beckman and Eric Meyer. The first 36 minutes ... I didn’t understand anything, but
starting from about the 36th minute it becomes
really interesting.
In short, let's reformulate the idea of a linear group of objects in Swift, as well as an object that can iterate over this linear group. You can do this by defining these fake Swift protocols:
Doubles
Let's turn it all over and make
doubles . We will send data to where they came from. And get the data from where they left. It sounds absurd, but bear with it a little.
Double Enumerable
Let's start with Enumerable:
Since
getEnumerator()
took
Void
and gave
Enumerator
, now we accept the [double]
Enumerator
and give
Void
.
I know this is strange. Do not leave.
Double Enumerator
And then what is
DualOfEnumerator
?
There are several issues here:
- There is no concept of a set-only property in Swift.
- What happened with
throws
in Enumerator.moveNext()
?
- What happens to
Disposable
?
To fix the problem with a set-only property, we can treat it as what it really is - a function. Let's
DualOfEnumerator
our
DualOfEnumerator
:
protocol DualOfEnumerator {
To solve the problem with
throws
, let's separate the error that may occur in
moveNext()
and work with it as a separate
error()
function:
protocol DualOfEnumerator {
We can do something else: take a look at the signature of the completion of the iteration:
func enumeratorIsDone(Bool)
Probably something similar will happen over time:
enumeratorIsDone(false) enumeratorIsDone(false)
Now, let's simplify things and call
enumeratorIsDone
only when ... everything is really ready. Guided by this idea, we simplify the code:
protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Take care of ourselves
What about
Disposable
? What to do with it? Since
Disposable
is part
of the Enumerator
type , when we get the
Enumerator
double , it probably shouldn't be in the
Enumerator
. Instead, it should be part of
DualOfEnumerable
. But where exactly?
DualOfEnumerator
here:
func subscribe(DualOfEnumerator)
If we accept
DualOfEnumerator
, then should not
Disposable
be
returned ?
Here's what kind of double you get in the end:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Call it a rose, though not
So, one more time, here's what we got:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Let's play a little with the names now.
Let's start with
DualOfEnumerator
. We’ll come up with better names for the functions to more accurately describe what is happening:
protocol DualOfEnumerator { func onComplete() func onError(Error) func onNext(Element) }
So much better and more understandable.
What about type names? They are just awful. Let's change them a bit.
-
DualOfEnumerator
- something that follows what happens to a linear group of objects. We can say that he observes a linear group.
-
DualOfEnumerable
is a subject of observation. What we are watching. Therefore, it can be called observable .
Now make the final changes and get the following:
protocol Observable { func subscribe(Observer) → Disposable } protocol Observer { func onComplete() func onError(Error) func onNext(Element) }
Wow
We have just created two fundamental objects in RxSwift. You can see their real versions
here and
here . Note that in the case of Observer, the three
on()
functions are combined into one
on(Event)
, where
Event
is an enumeration that determines what the event is - completion, next value or error.
These two types underlie RxSwift and reactive programming.
About fake protocols
The two fake protocols I mentioned above are actually not fake at all. These are analogues of existing types in Swift:
Well?
So what to worry about?
So much in modern development -
especially application development - is associated with asynchrony. The user suddenly clicked on a button. The user suddenly selected a tab in the UISegmentControl. The user suddenly selected a tab in the UITabBar. The web socket suddenly gave us new information. This download suddenly - and finally - ended. This background task ended abruptly. This list goes on and on.
In the modern world of CocoaTouch, there are many ways to handle such events:
- notifications
- callbacks
- Key-Value Observation (KVO),
- target / action mechanism.
Imagine if
it all could be reflected in one single interface. Which could work with
any kind of asynchronous data or events within the entire application.
Now imagine if there would be a whole
set of functions that allows you to modify these
streams , convert them from one type to another, extract information from Elements, or even combine them with other streams.
Suddenly, in our hands is a new
universal set of tools.
And so, we returned to the beginning:
API for asynchronous programming with observable threads.
This is what makes RxSwift such a powerful tool. Like Combine.
What's next?
If you want to read more about RxSwift
in practice , then I recommend
my five articles written in 2016 . They describe the creation of a simple CocoaTouch application, followed by a phased conversion to RxSwift.
In one of the following articles I will explain why many of the techniques described in my
article series for beginners are not applicable in Combine, and I also compare Combine with RxSwift.
Combine: what's the point?
The discussion of Combine also includes a discussion of the main differences between it and RxSwift. For me there are three of them:
- the possibility of using non-reactive classes,
- error processing,
- backpressure.
I will devote a separate article to each item. I'll start with the first one.
Features of RxCocoa
In
a previous post, I said that RxSwift is more than ... RxSwift. It provides numerous possibilities for using controls from UIKit in the type-but-not-quite subproject of RxCocoa. In addition, the
RxSwiftCommunity went further and implemented many bindings for even more secluded back streets of UIKit, as well as some other CocoaTouch classes that RxSwift and RxCocoa do not yet cover.
Therefore, it is very easy to get an
Observable
stream from, say, clicking on UIButton. I will give this example again:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Light weight.
Let's (finally) still talk about Combine
Combine is very similar to RxSwift. As the documentation says:
The Combine framework provides a declarative Swift API for handling values over time.
Sounds familiar: recall the description of ReactiveX (the parent project for RxSwift):
API for asynchronous programming with observable threads.
In both cases, the same thing is said. It's just that specific terms are used in the description of ReactiveX. It can be reformulated as follows:
An API for asynchronous programming with values over time.
Almost the same as for me.
Same as before
When I started analyzing the API, it immediately became obvious that most of the types I know from RxSwift have similar options in Combine:
- Observable → Publisher
- Observer → Subscriber
- Disposable → Cancellable . This is a triumph of marketing. You can’t imagine how many surprised looks I received from unbiased developers when I started describing Disposable in RxSwift.
- SchedulerType → Scheduler
So far so good. Once again, I like Cancellable much more than Disposable. A great replacement, not only in terms of marketing, but also in terms of an accurate description of the essence of the object.
More is even better!
This is not immediately clear, but spiritually they serve one purpose, and not one of them can give rise to errors.
"Break for poop"
Everything changes as soon as you start delving into RxCocoa. Remember the example above, in which we wanted to get an Observable stream that represents clicks on UIButton? There he is:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Combine requires ... much more work to do the same.
Combine does not provide any capabilities for binding to UIKit objects.
It's ... just an unreal bummer.
Here is a common way to get
UIControl.Event from
UIControl using Combine:
class ControlPublisher<T: UIControl>: Publisher { typealias ControlEvent = (control: UIControl, event: UIControl.Event) typealias Output = ControlEvent typealias Failure = Never let subject = PassthroughSubject<Output, Failure>() convenience init(control: UIControl, event: UIControl.Event) { self.init(control: control, events: [event]) } init(control: UIControl, events: [UIControl.Event]) { for event in events { control.addTarget(self, action: #selector(controlAction), for: event) } } @objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) { subject.send(ControlEvent(control: sender, event: event)) } func receive<S>(subscriber: S) where S : Subscriber, ControlPublisher.Failure == S.Failure, ControlPublisher.Output == S.Input { subject.receive(subscriber: subscriber) } }
Here ... a
lot more work. At least the call looks like:
ControlPublisher(control: self.button, event: .touchUpInside) .sink { print("Tap!") }
For comparison, RxCocoa provides a pleasant, tasty cocoa in the form of bindings to UIKit objects:
self.button.rx.tap .subscribe(onNext: { _ in print("Tap!") })
By themselves, these challenges are ultimately really very similar. The only frustrating thing is that I had to write
ControlPublisher
myself to get to this point. Moreover, RxSwift and RxCocoa are very well tested and are used in projects much more than mine.
For comparison, my
ControlPublisher
appeared only ... now. Only because of the number of clients (zero) and the usage time in the real world (almost zero compared to RxCocoa) can my code be considered infinitely more dangerous.
Bummer.
Community help?
Honestly, there is nothing stopping the community from creating their own open source “CombineCocoa” that fills the RxCocoa gap just like the RxSwiftCommunity did.
Nevertheless, I consider this a huge minus of Combine. I do not want to rewrite the entire RxCocoa, only to get bindings to UIKit objects.
If I decide to bet on
SwiftUI , then I suppose this will
eliminate the problem of lack of bindings. Even
my small application contains a
bunch of UI code. Throwing all this out just to jump onto the Combine train will be at least stupid, or even dangerous.
By the way, the article in the
Receiving and Handling Events with Combine documentation briefly describes how to receive and process events in Combine. The introduction is good, it shows how to extract a value from a text field and save it in a custom model object. The documentation also demonstrates the use of operators to perform some more advanced modifications to the stream in question.
Example
Let's move on to the end of the documentation, where the code example is:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel)
I have ... a lot of problems with this.
Notify you that I do not like it
The first two lines cause me the most questions:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField)
NotificationCenter is something like an application bus (or even a system bus) in which many can throw data, or catch pieces of information flying by. This solution is from the all-and-for-all category, as intended by the creators. And there really are many situations where you may need to find out, say, the keyboard was just shown or hidden. NotificationCenter is a great way to distribute this message throughout the system.
But for me
NotificationCenter is
code with a choke . There are times (like receiving a notification about the keyboard) when NotificationCenter is actually the
best possible solution to the problem. But too often for me NotificationCenter is the
most convenient solution. It is really very convenient to drop something in NotificationCenter and pick it up somewhere else in the application.
In addition, the NotificationCenter is
"string" -typed , that is, you can easily make the mistake of which notification you are trying to publish or listen to. Swift is doing everything possible to improve the situation, but the same NSString still lies under the hood.
About KVO
On the Apple platform, there has long been a popular way to receive notifications of changes in different parts of the code: key-value observation (KVO). Apple describes it like this:
This is a mechanism that allows objects to receive notifications of changes in the specified properties of other objects.
Thanks to a
Gui Rambo tweet, I noticed that Apple added KVO bindings to Combine. This could mean that I could get rid of the many disappointments about the lack of an RxCocoa analogue in Combine. If I can use KVO, this will probably eliminate the need for CombineCocoa, so to speak.
I tried to figure out an example of using KVO to get a value from a
UITextField
and output it to the console:
let sub = self.textField.publisher(for: \UITextField.text) .sink(receiveCompletion: { _ in print("Completed") }, receiveValue: { print("Text field is currently \"\($0)\"") })
Looks good, move on?
Not so fast, friends.
I forgot about the
uncomfortable truth :
UIKit, by and large, is not compatible with KVO.
And without KVO support, my idea would not work. My checks confirmed this: the code does not output anything to the console when I enter text in the field.
So, my hopes of getting rid of the need for UIKit bindings were beautiful, but not for long.
Cleaning
Another Combine problem is that it is completely unclear where and how to free resources in
Cancellable objects.
It seems that we should store them in instance variables. But I don’t remember that in the official documentation something was said about the cleaning.
RxSwift has a terribly-named-but-incredibly-convenient
DisposeBag . It is no less easy to create CancelBag in Combine, but I'm not quite sure that in this case it is the best solution.
In the
next article we will talk about error handling in RxSwift and Combine, about the advantages and disadvantages of both approaches.