Tech

Taming Great Complexity: MVVM, Coordinators and RxSwift

Updated on
June 8, 2023
Table of content
Show
HIDE

Last year our team started using Coordinators and MVVM in a production app. At first it looked scary, but since then we’ve finished 4 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.

Example

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.

app screen
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.

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:

CODE:https://gist.github.com/arthur-here/0da4e749e12a5bc8ca989002fb8a226c.js

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:

  • 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.

RxSwift

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 guide or 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.

CODE:https://gist.github.com/arthur-here/e6db66299486c0b123410b4a63188eae.js

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:

CODE:https://gist.github.com/arthur-here/331e9af250a2e1c8f8f408b850cba4df.js

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”)

fileprivate let currentLanguage = BehaviorSubject(value: “Swift”)

Outcomes

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

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:

CODE:https://gist.github.com/arthur-here/48129113ef2edcab038cc748d8f8f223.js

Next we will move all our data mutation and formatting code from the RepositoryListViewController into RepositoryListViewModel:

CODE:https://gist.github.com/arthur-here/8eb3b1b32b46271aad5037604df78172.js

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:

CODE:https://gist.github.com/arthur-here/1b61e406a38ed07f12e0084a59a299b4.js

Outcomes

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.

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.
Coordinators Flow
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:

CODE:https://gist.github.com/arthur-here/30778b4e1e58c59f07f76876e613a82a.js

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;
  • 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:

CODE:https://gist.github.com/arthur-here/e6b036e6aaf4bd3df45cce954d0e4926.js

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.

CODE:https://gist.github.com/arthur-here/100c5f849ea9238cd083b9e688be6c24.js

Notice that we return Observable.never() because Repository List screen is always in the view hierarchy.

Outcomes

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:

MVVM-C architecture
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.

Conclusion

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.