Using the MVI pattern in Swift iOS App development
In the last couple of years, several new patterns emerged, all praising to make a developer’s life as easy as never before. By separating certain parts of the codebase, each of these patterns tries to make code more readable, easier to test, and eventually more maintainable.
This tutorial assumes a basic understanding of RxSwift, RxCocoa, Databindings, and how to use Cocoapods. Thus I won’t go into detail regarding these topics. Prior knowledge about other MVI* patterns does help but is not required.
MVI(Model-View-Intent) isn’t an entirely new pattern. Instead, it is built upon existing principles that have been used in the industry for years now. As the name already states, MVI adds to the list of MV* patterns. While these designs are traditionally imperative, reactive extensions have become more famous during the last decade. Especially concepts like MVVM moved closer to reactive programming, sometimes replacing imperative paradigms completely.
MVI was designed with reactive programming in mind. By using a framework, such as ReactiveX(http://reactivex.io), it unleashes its full potential, placing it further away from its siblings. While the imperative approach is possible, it counteracts the usefulness of MVI. Keeping that in mind, one should have a basic understanding of reactive programming before working with a pattern such as MVI.
The MV* family
As stated before, MVI is an addition to the MV* patterns family. Especially when compared to the MVP pattern, one might get the impression these two patterns are the same. Components are often named identical, but the use and connection of them are different.
The most common MV* patterns are:
Let’s first have a look at the elder members of MV*.
Looking at the difference between MVC and MVP, we can see that MVP already removes one level of dependencies. The model and the view don’t interact with each other anymore, making the code easier to test. For iOS apps, this does also prevent the Massive View Controller problem. Since our codebase is split between the Presenter and the ViewController, each of them is smaller by nature.
More decoupling and events
To further decouple software components, MVVM got invented. It usually makes use of bindings, making it more event-driven than the other MV* patterns by nature. One issue of MVVM is that code is harder to test than an MVP codebase as models and ViewModels need to be mocked. Another problem of MMVM is that ViewModels need to be mutable since the view gets updated by ViewModel state updates(Usually via observers/bindings).
MVI to the rescue
MVP and MVVM are already great patterns for projects of any size, with MVC rarely being used in projects any bigger than small apps, but none of them was built with the reactive concept in mind. Our code becomes way simpler as we make use of many extensions already present in most reactive frameworks, while readability also increases. MVI also decouples our components even further, eventually making the control flow completely unidirectional. When done right, MVI dependencies are laid out like a circle.
Let’s have a look at this MVI diagram.
The first thing that stands out is the lack of our model. The reason for that is that the MVI models are states on their own. Instead of having a ViewModel notify our view about new models, our model itself is the state. From this come several benefits:
- Our models are no longer mutable.
- Each model is a state a view can enter
- Bugs can be found faster as we can focus on the state in which they occurred
- States can be made persistent very easily
Looking further at the image, we see that we have a new component called the Intent. An intent is an interaction a user triggers that is then carried out. The Intent component is often called the Presenter, such as in MVP, but it’s implemented differently. Thus I kept the name Intent here.
Let’s have a look at MVP and MVI side by side again before we start developing our app.
We can see the model is still there technically. It’s just not used in the same way as in MVP. MVP uses models as data containers while models in MVI are states our app can enter. For example, we could have a model that indicates that the app should show a loading bar while another state could hold data to be displayed.
The project you create will be a simple app in which you can choose between heroes of your choice. When a hero is selected, an alert will be shown, displaying the name of your hero. I created a repository you can clone to see it in action. https://github.com/broken-bytes/Swift-MVI-pattern
Create a new XCode iOS project, leave everything as it is. We don’t need to customize anything, as this will be a simple single view app. After the initial setup, close XCode. Then, run pod install from your command line in your project directory. Replace the generated Podfile with the following code.
target ‘MVI-Swift’ do
pod ‘RxSwift’, ‘~> 5’
pod ‘RxCocoa’, ‘~> 5’
From now on, you will only use the generated xcworkspace, so open it now.
Gluing it all together
Rename ViewController.swift to MainViewController.swift and change references to it, then recreate the design above in your storyboard. The exact appearance does not matter, but it helps to keep the same components as those are referenced in our MainViewController later. Note that the bars are ProgressView elements. Buttons for selecting the previous and next characters are left and right of the screen, accordingly. Uppermost of the screen, a label shows the name of the current hero.
Let’s first create our models and business logic, so we don’t run into errors due to missing components later.
Create a file called Hero.swift. Pass the following code.
Side note: In our app, we assume that an image asset with the same name as our hero is present.
The next file is an MVI specific component. In MVVM, this is similar to a ViewModel, expect for immutability. It is our State class and its default states. Create a file called HeroState.swift. Once again, paste the following code.
First, we define a protocol every state has to conform to. Then we define our data state, called HeroPresenting. When our view enters this state, it updates its elements with the data received. Next, we have a state called HeroSelected, which indicates that the user has selected a hero via the select button found at the very bottom of the view. States can report arbitrary information to the view, which it then processes. That makes MVI a robust design as with just a few properties, the behavior of our view can be changed completely.
Having the model and state part covered, we can now create our business logic. For our app, a class will simulate our API calls. Create a file named HeroSelector.swift and paste the following code.
The code is straightforward. We create properties and methods that return the previous, next, and current hero. We also implement checks to see if each of these directions is possible. The heroes list is our “Database.” We populate it with some items so we can test our app.
What comes now is the part- besides the state, which still shares some concepts with MVVM- that is specific only to MVI. Create a new file called MainIntent.swift. Paste the following code.
We declare a PublishRelay, which will trigger a view update on new values being accepted.
Next, we reference the view this intent should be bound to. HeroSelector is our business logic. It contains a simple list of heroes and their stats. In an actual app, this would be our backend or storage calls.
The DisposeBag property is used to simplify the dispose process of our disposables.
Init() is self-explaining, all it does it create a relay and assigns it to our property.
The most important part of this class is the bind() method. We subscribe to value updates on our relay and update our view with the new state received. When bound, we emit a default value, which is the first item of our list.
onPreviousCharacterClicked() and onNextCharacterClicked() both get the last or next hero from our business logic level and update the relay accordingly via the presentHero() method.
Inside presentHero(), we check if there is a previous and next hero in the list via the provided methods by heroSelector. Then, we create a new state object and pass the hero and boolean variables we fetched before, which is then supplied to our relay.
Talking about states onSelectCharacter() and onDismissCharacter() both set states as well. While onSelectCharacter() sets the HeroSelected state, onDismissCharacter() gets the last active hero and sets the HeroPresenting state again. Once again, we pass an immutable state object to our relay, which is then used by the view via its update() method.
Next, replace your MainViewController.swift with the following script.
Then, drag your interface elements onto the Outlets.
Our first non-outlet property is the intent of this View. In an actual project, the view should not hold a reference to its intent. Instead, the View should be passed to the intent via DI(Dependency Injection). Dependency injection is an essential part of MVI as it allows us to decouple each part of the code altogether, eventually reducing dependencies to a minimum. For the sake of this article, the view will create the intent.
In our init(), we set the button bindings and pass the view to the intent. Instead of using OutletActions, we have our bindButtons() method, which uses RxCocoa extensions to observe the press of the buttons. Each button press event is bound to the corresponding method of our intent.
The update() method is the heart of an MVI view. It is often called render, but I find this name misleading as the actual rendering is lower level. It checks what state was passed and executes different tasks based on the type of it. When given a HeroPresenting state, it assigns the values to the matching UI elements. As for HeroSelected, it displays an Alert that was defined in init(). The message of the alert is changed to the last selected hero name. The only action it contains is a dismiss button, that advises the intent to show the HeroState.HeroPresenting state again. That particular state is the same one as before the select happened, which indicates yet that states can easily be written to persistent storage as they are retrievable.
Testing it out
You should now be able to build your app
When clicking either the left or right button next to the image, the current hero should change. Selecting a hero via the bottom button should also work. To check if the states update correctly, you can add a print statement at the start of the MainViewController update method that shows the type of the state passed.
You saw what a simple MVI app could look like in a simple example. Using reactive programming, we made our code very easy to read and maintainable. While going through the code examples, we noticed that testability and decoupling are very easy to achieve in MVI. While MVI has some overhead due to bindings and its learning curve is steep for people without knowledge about reactive programming, it can make a massive difference in certain projects.
MVI is a great architecture. It makes our code readable, very easy to extend by just adding new states, and rock-solid by making debugging a ton easier. And yet I would recommend against it unless 1. You or your team has previously touched reactive design and 2. Your app handles multiple states and is large enough that MVC or MVP is not suitable anymore.
These arguments might change if you are developing for multiple platforms. With frameworks such as ReactiveX, code can be written in a form that brings each platform closer together, eventually enabling developers to understand other platform’s code better, making teams more productive. The reason for that is that ReactiveX has a framework for almost any modern important language.
In my opinion, MVI is the architecture that is best used as a replacement for MVVM. If MVC works great for your small app project, why wrap your head around new code paradigms. If MVP is what you have been using for your apps for years, but you want to tackle a more reactive-like design, try MVI. If you don’t feel comfortable changing your pattern, MVP can be reactive, just like MVI. It, of course, suffers from its flaws of being designed more imperatively and less decoupled. But if you are open to new designs, I would suggest that you replace your MVVM stack with MVI for your next project. Once you get the hang on it, it can kickstart your productivity.
- What can be simplified further using MVI?
- How would MVI fit in a design such as DDD?
- Is MVI faster than MVVM regarding performance in heavy apps?
- How would one write unit tests for MVI under Swift
- What frameworks work best with MVI in Swift?