Our team has been using Coordinators and MVVM in production apps for more than 2 years. At first, it looked scary, but since then we’ve finished 6 applications built on top of those architectural patterns. In this article, I will share our experience and will guide you to the land of MVVM, Coordinators & Reactive programming.
Instead of giving a definition up front, we will start with a simple MVC example application. We will do the refactoring slowly step by step to show how every component affects the codebase and what are the outcomes. Every step will be prefaced with a brief theory intro.
In this article, we are going to use a simple example application that displays a list of the most starred repositories on GitHub by language. It has two screens: a list of repositories filtered by language and a list of languages to filter repositories by.
A user can tap on a button in the navigation bar to show the second screen. On the languages screen, he can select a language or dismiss the screen by tapping on the cancel button. If a user selects a language the screen will dismiss and the repositories list will update according to the selected language.
You can find the source code here:
Coordinator-MVVM-Rx-Example — Example of MVVM-C architecture implemented with RxSwiftgithub.com
The repository contains 4 folders: MVC, MVC-Rx, MVVM-Rx, Coordinators-MVVM-Rx correspondingly to each step of the refactoring. Let’s open the project in the MVC folder and look at the code before refactoring.
Most of the code is in two View Controllers:
LanguageListViewController. The first one fetches a list of the most popular repositories and shows it to the user via a table view, the second one displays a list of languages.
RepositoryListViewController is a delegate of the
LanguageListViewController and conforms to the following protocol:
RepositoryListViewController is also a delegate and a data source for the table view. It handles the navigation, formats model data to display and performs network requests. Wow, a lot of responsibilities for just one View Controller!
Also, you could notice two variables in the global scope that define a state of the
repositories. Such stateful variables introduce complexity to the class and are a common source of bugs when parts of our app might end up in a state we didn’t expect. To sum up, we have several issues with the current codebase:
- View Controller has too many responsibilities;
- we need to deal with state changes reactively;
- the code is not testable at all.
Time to meet our first guest.
The component that will allow us to respond to changes reactively and write declarative code.
What is Rx? One of the definitions is:
ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.
If you are not familiar with functional programming or that definition sounds like a rocket science (it still does for me) you can think of Rx as an Observer pattern on steroids. For more info, you can refer to the Getting Started guideor to the RxSwift Book.
Let’s open MVC-Rx project in the repository and take a look at how Rx changes the code. We will start from the most obvious things to do with Rx — we replace the
LanguageListViewControllerDelegate with two observables:
LanguageListViewControllerDelegate became the
didCancel observables. We use them in the
prepareLanguageListViewController(_: ) method to reactively observe
Next, we will refactor the
GithubService to return observables instead of using callbacks. After that, we will use the power of the RxCocoa framework to rewrite our View Controllers. Most of the code of the
RepositoryListViewController will move to the
setupBindings function where we declaratively describe a logic of the View Controller:
Now we got rid of the table view delegate and data source method in view controllers and moved our state to one mutable subject:
fileprivate let currentLanguage = BehaviorSubject(value: “Swift”)
We’ve refactored example application using RxSwift and RxCocoa frameworks. So what exactly it gives us?
- all the logic is declaratively written in one place;
- we reduced state to one subject of current language which we observe and react to changes;
- we used some syntactic sugar from RxCocoa to setup table view data source and delegate briefly and clearly.
Our code still isn’t testable and View Controllers still responsible for a lot of things. Let’s turn to the next component of our architecture.
MVVM is a UI architectural pattern from Model-View-X family. MVVM is similar to the standard MVC, except it defines one new component — ViewModel, which allows to better decouple UI from the Model. Essentially, ViewModel is an object which represents View UIKit-independently.
The example project is in the MVVM-Rx folder.
First, let’s create a View Model which will prepare the Model data for displaying in the View:
Next, we will move all our data mutation and formatting code from the
Now our View Controller delegates all the UI interactions like buttons clicks or row selection to the View Model and observes View Model outputs with data or events like
We will do the same for the
LanguageListViewController and looks like we are good to go. But our tests folder is still empty! The introduction of the View Models allowed us to test a big chunk of our code. Because ViewModels purely convert inputs into outputs using injected dependencies ViewModels and Unit Tests are the best friends in our apps.
We will test the application using RxTest framework which ships with RxSwift. The most important part is a
TestScheduler class, that allows you to create fake observables by defining at what time they should emit values. That’s how we test View Models:
Okay, we’ve moved from MVC to the MVVM. But what’s the difference?
- View Controllers are thinner now;
- the data formatting logic is decoupled from the View Controllers;
- MVVM made our code testable.
There is one more problem with our View Controllers though —
RepositoryListViewController knows about the existence of the
LanguageListViewController and manages navigation flow. Let’s fix it with Coordinators.
If you haven’t heard about Coordinators yet, I strongly recommend reading this awesome blog post by Soroush Khanlou which gives a nice introduction.
In short, Coordinators are the objects which control the navigation flow of our application. They help to:
- isolate and reuse ViewControllers;
- pass dependencies down the navigation hierarchy;
- define use cases of the application;
- implement deep linking.
The diagram shows the typical coordinators flow in the application. App Coordinator checks if there is a stored valid access token and decides which coordinator to show next — Login or Tab Bar. TabBar Coordinator shows three child coordinators which correspond to the Tab Bar items.
We are finally coming to the end of our refactoring process. The completed project is located in the Coordinators-MVVM-Rx directory. What has changed?
First, let’s check what is
That generic object provides three features for the concrete coordinators:
- abstract method
start()which starts the coordinator job (i.e. presents the view controller);
- generic method
coordinate(to: )which calls
start()on the passed child coordinator and keeps it in the memory;
disposeBagused by subclasses.
Why does the
start method return an
Observable and what is a
ResultType is a type which represents a result of the coordinator job. More often
ResultType will be a
Void but for certain cases, it will be an enumeration of possible result cases. The
start will emit exactly one result item and complete.
We have three Coordinators in the application:
AppCoordinatorwhich is a root of Coordinators hierarchy;
Let’s see how the last one communicates with ViewController and ViewModel and handles the navigation flow:
Result of the LanguageListCoordinator work can be a selected language or nothing if a user taps on “Cancel” button. Both cases are defined in the
RepositoryListCoordinator we flatMap the
showLanguageList output by the presentation of the
LanguageListCoordinator. After the
start()method of the
LanguageListCoordinator completes we filter the result and if a language was chosen we send it to the
setCurrentLanguageinput of the View Model.
Notice that we return
Observable.never() because Repository List screen is always in the view hierarchy.
We finished our last stage of the refactoring, where we
- moved the navigation logic out of the View Controllers and isolated them;
- setup injection of the View Models into the View Controllers;
- simplified the storyboard.
From the bird’s eye view our system looks like this:
The App Coordinator starts the first Coordinator which initializes View Model, injects into View Controller and presents it. View Controller sends user events such as button taps or cell section to the View Model. View Model provides formatted data to the View Controller and asks Coordinator to navigate to another screen. The Coordinator can send events to the View Model outputs as well.
We’ve covered a lot: we talked about the MVVM which describes UI architecture, solved the problem of navigation/routing with Coordinators and made our code declarative using RxSwift. We’ve done step-by-step refactoring of our application and shown how every component affects the codebase.
There are no silver bullets when it comes to building an iOS app architecture. Each solution has its own drawbacks and may or may not suit your project. Sticking to the architecture is a matter of weighing tradeoffs in your particular situation.
There’s, of course, a lot more to Rx, Coordinators and MVVM than what I was able to cover in this post, so please let me know if you’d like me to do another post that goes more in-depth about edge cases, problems and solutions.
Thanks for reading!
Arthur Myronenko, UPTech Team With ❤️
This post was originally published at UPTech Team blog. Follow us for more articles on how to build great products 💪