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.
Screens of the example app
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:
uptechteam/Coordinator-MVVM-Rx-Example_Coordinator-MVVM-Rx-Example — Example of MVVM-C architecture implemented with RxSwift_github.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: RepositoryListViewController
and 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:
The 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 RepositoryListViewController
: currentLanguage
and 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:
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: didCancel
and didSelectLanguage
.
LanguageListViewControllerDelegate
became the didSelectLanguage
and didCancel
observables. We use them in the prepareLanguageListViewController(_: )
method to reactively observe RepositoryListViewController
events.
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?
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 RepositoryListViewController
into RepositoryListViewModel
:
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 showLanguageList
.
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?
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:
Coordinators Flow
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 BaseCoordinator
:
Base Coordinator
That generic object provides three features for the concrete coordinators:
start()
which starts the coordinator job (i.e. presents the view controller);coordinate(to: )
which calls start()
on the passed child coordinator and keeps it in the memory;disposeBag
used by subclasses.Why does the _start_
method return an _Observable_
and what is a _ResultType_
?
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:
AppCoordinator
which is a root of Coordinators hierarchy;RepositoryListCoordinator
;LanguageListCoordinator.
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 LanguageListCoordinationResult
enum.
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 setCurrentLanguage
input 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
From the bird’s eye view our system looks like this:
MVVM-C architecture
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 💪