Scalio

iOS-RxSwift

Simplifying RxSwift code

Anton Nazarov

Anton Nazarov

Senior iOS Engineer

Simplifying RxSwift code


I promised myself once: “I will never write an article about architecture, cause it is the banalest topic”. I have never been so wrong in my life. Here is the overview of reactive architecture evolution in my projects, since the first pod 'RxSwift' till a few years of reactive stack programming.

 

Disclaimer

No silver bullet. Only experience with production examples and opinions on different cases.


Code samples are implemented with RxSwift, however, all highlighted problems are relevant for any reactive solutions (e.g. Combine).


Check out this repo with examples for better understanding. Folders inside Screen/Search corresponds with topics of this article.

 

Introduction

When a developer makes his first steps in reactive programming, he or she quickly realizes that there are two clearly separated components in every screen he implemented.


First is about the poor mapping of user intents and binding them to business logic inputs. For instance, a registration screeцn with few fields and “Sign Up” button can be implemented like this:

View


It’s clear that a user has only one intent signup. It combines all inputs from UI and emits only when the signup button is tapped.


The second component encapsulates your business logic. Probably, it’s something about mapping and filtering events representing user’s intents. We want to map user signup intent to the corresponding network request. Note, that flatMapLatest & catchError idiom helps us to prevent completion of the sequence. A new request will be sent every time button is pressed.

ViewModel


These are basic examples and, of course, you saw the names of these components many times. They are widely known as View and ViewModel. And together they are the most popular architecture for RxSwift or something which has UI-bindable properties. It’s called MVVM.


It was always absolutely clear for me where to put business logic and how to implement it. The only complicated question is how to create a convenient connection between these two layers. Connection, which will be comfortable for tests, refactoring and extending. And, in my opinion, this connection is the most important place in reactive MVVM.


My team in scal.io walked a long way trying to find the solution completely suitable for our needs and I am going to present you a breath overview. I hope this will save you a lot of time and reduce pain :)


 

Input as functions

Input as functions


Honestly, this was not the first implementation I used. The initial version had var dataSource: Observable<[Item]>. But I quickly realized that RxSwift has a powerful system of traits and using them is actually a good idea. Traits help to create meaningful interfaces. Driver here means that dataSource is needed for UI stuff, probably table configuration, so items should be observed on the main thread.


User is able to search or select items, we formalize these intents as functions. But we quickly realize that it’s not convenient. Look at this part of code, where few operators are applied to searchBar.rx.text stream.

From SearchViewController


These lines should not be here. Their place is ViewModel because debouncing, filtering and skipping is actually business logic. But search is a function, it has no stream semantic. This function is imperative and we can’t use stream operators there, so there is no chance to move these lines to ViewModel.


Of course, we can implement operators in imperative style… But I can’t see any sense in this double work. RxSwift methods are well optimized and covered by tests. They are trustable and it’s better to prefer them over the self-invented wheel. That’s why functions should not be used in ViewModelcontract.

 

Input as streams

User intent is a stream of events, this is a really good idea. We already have these concepts in button.rx.tap or searchBar.rx.text, so what’s the point to rethink it? Let’s use the same approach for communication of our architecture components.


Streams as init parameters


The first thought is to pass all inputs from UI to ViewModel via init like in RxExample project.

Passing UI events to ViewModel via init


This might look like a good idea, however, I don’t think it really is. IMO, init should take only arguments which are necessary for object creation. Can the ViewModel live without a stream of login button taps? Definitely, yes. So input tuple should not be here.


If I didn’t convince you, think a bit more about dependency injection. You can get required streams of UI elements only after view was loaded and outlets were injected. So, you can’t create your ViewModel before loading a view. viewDidLoad method will be called with nil viewModel and only after it you will get all necessary inputs.


This approach doesn’t look safe for me. A view should be loaded only when ViewController is fully initialized and ready to work.


Input subjects


Let’s just declare ViewModel properties which will be responsible for input streams.

Input as Subjects


And that’s it. Our ViewController is just a simple namespace for declaring bindings of UI elements to business logic inputs. Just look at these lines:

Absolutely declarative


Absolutely declarative and clean. Perfect? No. Actually, it became even worse from the contract point of view. Let’s remember this picture from Subject documentation.


 

Subject completes all subscription when getting .completed event


Subject conforms to Observer protocol, so it can errors out or complete. Subscriptions of that subject will repeat that behavior. So, if we emit an error to search subject, our UI will be completely dead. Only application relaunch will recreate this subscription again.


Yes, searchBar.rx.text has ControlProperty type, which never fails, so it’s safe to bind it to the subject. However, it’s bad to have a contract, which allows actions which we don’t require to happen. So, it’s incorrect to use Subject for expressing user intents.


Input relays


Happily, RxSwift already handled that for us. One more abstraction around Observer was introduced. Relay just ignores .completed events and raise a fatal error in Debug mode, you can read more about it here. In 5.0.0 release relays were moved to separate framework, so they can be used even without RxCocoa.

Input as Relays


Yes! Inputs and outputs can’t fail or complete. The contract is now absolutely correct. And I would like to finish this article exactly here, but I don’t want to lie to you.


The main issue — my example is too simple. This is the problem of all architectures sample projects, they work awesome only with the counter app. But when trying to scale such solutions to a production application, we meet problems. So does I.

 

I/O Mess

It all starts with a really simple screen. You saw it many times: table view, pagination, activity indicator and pull to refresh. Something like that:Diagram


Even with this low complexity, code can be unclear. To be honest, I love this style and it’s easy to read for me. But I understand why it can be confusing for a new developer, which is unfamiliar with FRP.


The reason is that we can’t make a pure function from our ViewModel. There is no opportunity to implement business logic without having internal states and side effects.


For instance, within this search app example, I have to store the current page to make pagination works. I can’t just take a stream of reachedBottomevents and map it to next page items, cause I don’t have page parameter initially. I have to store and mutate it.


I have to combine many inputs and internal states to create one output. I have to split operators chain and save intermediate stream state just to bind it to multiple outputs. That’s why there are newSearchRequests andallSearchRequests needless variables. Just to correctly handle loading and save pagination state. Trust me, the more complicated logic you have, the more messy Rx code you will get.


I have a production example when table view content depends on six different streams:

  • Current device geolocation
  • Custom location selected by a user
  • Currently scrolled page
  • Search query
  • Pull-to-refresh events
  • Responses from two separate endpoints

Not really a simple screen with many dependencies between inputs/outputs


And god, how hard it was to implement it in a readable way. One day I finally realized I just can’t do it with this Inputs/Outputs ViewModel. And at that moment I decided to follow advice, which my granddad gave me when I was a child.

 


If you are looking for a solution of complicated problem, just go to Web. They already invented everything you need.


 

UDF

Yes, of course. Redux will save me. But I don’t want to implement async work in middleware. I need something which is closely integrated with Rx, built with its power in roots.


Currently, I know only two frameworks which combine the beauty of Action/State idea with RxSwift power: RxFeedback and ReactorKit. They both allow us to reach the purpose:

  • combine all inputs into one stream containing enum of user intents
  • combine all outputs into one stream of state objects

What we actually get from UDF


ReactorKit


I only used ReactorKit in production, so here are its benefits over pure RxSwift architecture. Repo has detailed documentation, I will provide only a breath highlight of benefits.


Action enum formalizes all possible user intents. State is a simple struct that incapsulates screen state. And Mutation enum formalizes all possible changes which can be applied to the state.


My favorite thing here is that mapping user intents to business logic requests are clearly separated from mutation of the state. Each Action should be mapped to Observable<Mutation>. When mutations stream emits, reduce the function is called with current state and new change. And god, it has never been so easy to change state.



Just look at this code, it became much more:

  • readable, no more complicated combining Rx operators, no more messy bindings to multiple outputs
  • simpler, it’s really easier to apply changes in an imperative way
  • explicit, all mutations are now collected in one place (not in random do(onNext:) at 300th line), easy to debug them
  • scalable, with the increasing complexity of business logic, you will just add a few more cases here
  • testable, it’s very easy to test this function, cause it’s pure.


 

In Closing

Good knowledge of a reactive framework is not enough to build a scalable application. Reactive code should be organized somehow, and you should set it as a standard in your team. This will help every dev easily change and extend code pieces.


Today I showed you my team standard progress. This implementation of UDF is called MVI and widely used in Android world. I suggest you to deeply think about the question “Does my reactive code is really as simple as I want it to be?”. And if not, probably MVI is what you actually need.


Try to adopt it within one screen and I promise, you will want more of it. And, remember, let Rx go in your heart.


 


I am going to share more RxSwift experience on Saint AppsСonf.  Meet me there!

 

How can we help you?