Getting Ready for Combine





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:



//   ;     //    Array. protocol Enumerable { associatedtype Enum: Enumerator associatedtype Element where Self.Element == Self.Enum.Element func getEnumerator() -> Self.Enum } // ,       . protocol Enumerator: Disposable { associatedtype Element func moveNext() throws -> Bool var current: Element { get } } //          // Enumerator,         .   . protocol Disposable { func dispose() }
      
      





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:



 //    ,  . protocol Enumerable { associatedtype Element where Self.Element == Self.Enum.Element associatedtype Enum: Enumerator func getEnumerator() -> Self.Enum } protocol DualOfEnumerable { //  Enumerator : // getEnumerator() -> Self.Enum //    : // getEnumerator(Void) -> Enumerator // //  , : // : Void; : Enumerator // getEnumerator(Void) → Enumerator // //     Void   Enumerator. //   -      Enumerator,   Void. // :  Enumerator; : Void func subscribe(DualOfEnumerator) }
      
      





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



?



 //    ,  . protocol Enumerator: Disposable { associatedtype Element // : Void; : Bool, Error func moveNext() throws -> Bool // : Void; : Element var current: Element { get } } protocol DualOfEnumerator { // : Bool, Error; : Void //   Error    func enumeratorIsDone(Bool) // : Element, : Void var nextElement: Element { set } }
      
      





There are several issues here:





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 { // : Bool; : Void, Error //   Error    func enumeratorIsDone(Bool) // : Element, : Void func next(Element) }
      
      





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 { // : Bool, Error; : Void func enumeratorIsDone(Bool) func error(Error) // : Element, : Void func next(Element) }
      
      





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) //     enumeratorIsDone(true)
      
      





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.





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:





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:





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:





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.



All Articles