Photo by KOBU Agency on Unsplash
You might already be familiar with MVVM even MVVM-C and even (jokes) MVC. But MVI? You might have heard of the Flux, Redux and MobX React patterns and you might not be aware that in native development this is actually MVI.
This article explores a sample project that uses just RxSwift for implementation. Wow.
Difficulty: Beginner | Easy | Normal | Challenging
This article has been developed using Xcode 11.4.1, and Swift 5.2.2
Coding in Swift Playgrounds (guide HERE) Some knowledge of RxSwift would be useful (guide HERE)
MVI: Model-View-Intent Redux: an open-source JavaScript library for managing application state
MVI is another architecture that looks at improving the separation of concerns when developing Apps. Those traditional patterns, MVC, MVVM and others (VIPER) are traditionally imperative. The issue is that reactive programming has become more popular, and there is nothing wrong with using RxSwift to create the bindings on MVVM.
In steps MVI, an architecture that is designed with reactive programming, where the model represents the state - the source of truth - of the application.
The Intent is an interaction that the user triggers and then carries out (often called a presenter, in architectures like MVP - which can be the ViewController in Apple's standard iOS architecture). In doing so, it makes code readable and extendable (through adding new states) and easy to debug by featuring a finite number of states.
The View Observes the user actions and is responsible for visibly representing the model to the user. This interaction modifies the Intent that manipulates the Model.
The more observant of you, will see that this is a cycle. You should also NOT be misled by the Model, the model here represents a unique state that represents the source of truth for the App.
- Models are not mutable
- Each model is a state
- Bugs focus on a particular state
- States can be made persistent
- Realm
- CoreData
Every Observable
sequence is just a sequence, and it can receive elements asynchronously.
A subject maintains a list of observers, that it notifies of changes. This is part of KVO which is an implementation of the observer pattern. Each subject can either act as an observable sequence (see below) and can be subscribed to, or act as an observer which means that new elements can be added onto a subject which will then be emitted to the subject subscribers - or a subject can act as both.
In RxSwift, everything is a sequence. This is the idea that everything is a data stream - it can even be referred to as an observable or a sequence (both of which are correct, we are dealing with sequences that are observable. These observable sequences can emit zero or more elements. If an error occurs, the observable sequence can emit an error event
RxSwift makes it easy to detect UIControlEvents
through use of rx.bind
. In this article, a UIButton
called displayPerson
and we can hook into this control by using rx.tap
:
displayPerson.rx.tap.bind { print ("tapped") }.disposed(by: disposeBag)
This ensures that the memory is properly deallocated and the memory correctly released. Disposables that are returned from subscriptions are usually inserted into a DisposeBag()
that is declared in a view controller with the following property
var disposeBag = DisposeBag()
which when (the DisposeBag()
) is deallocated it's deinit function calls dispose on all of the disposables it contains. Each disposable releases references to any observables that they are observing, which in turn release references to any disposables they are observing until everything is released.
This is a relay that (once subscribed to) emits all of the subsequently observed items to the subscriber. This is discrete from BehavorSubject that emits the most recently emitted item from the observable.
We can publish the event by using accept(_ event: Element)
which is then emitted to observers of the observable.
This project will have a simple button, and then present information as stored in the model on the screen. This will just be a Person with a name - this is a basic project after all!
We create a Single View Project, and then we need RxSwift
.
We can use Swift Package Manager for this, https://github.com/ReactiveX/RxSwift Up to next Major.
Now if you are worried about the size of your binary only add RxSwift and RxCocoa - I'm insane so I added the lot (It is a good idea to add tests into your project in any case! However, if you do do this make sure to add RxBlocking and RxTest to your Test target rather than your App target - you have been warned!).
In order to bind a button we need to import RxSwift
using import RxSwift
at the top of your ViewController
class. If you've already put a UIButton
onto the view in the Storyboard the following code will work a treat (taken from the example MVIDemo
written below).
Included in this repo I've made a nice little demo. I say "nice", but the user interface isn't. What also isn't particularly nice is what the app actually does.
Let me explain.
This particular App will have a an array of people
let people: [Person] = [Person(name: "James"), Person(name: "Ahmed")]
and will display the Person's name one at a time starting with the initial person. When the next button is pressed, the next person in the list is displayed.
Yes it is ugly.
Yes it is just a UILabel
on an view controller with a UIButton
at the bottom. There's nothing exciting here - this is intended to be an architecture article so please do bear with me.
Perhaps the least interesting part of this code is the person struct. This is going to be the struct
that represents the person and has a property containing the person's name. Nice (isn't it?). This single struct actually represents the state of our App (as we will see in the next section of this code)
The store is declared as a BehaviourRelay
so can be observed, and is set up with an initial State
value.
The State
can be referred to as a model
Being a nice Swift developer, using a
protocol and conforming to that protocol
for both the InitialState
and AppState
is the right things to do in that language.
The reducer
is responsible for returning a new State
based on the actions that it receives from the Intent
. For simplicity (as we are not using any external frameworks here) the reducer is represented by a struct
and the single action we have (that will go to the next name) will call a func
that moves us to the next element. The MVI model (which is the AppState
!) is immutable - meaning that the getNext
func
takes the current State
from the store and bases a new AppState
on that - returning a new initialised State
.
The Intent is also known as actions. Here the action is initialited by the user who clicks the button on the view controller. The intent then requests a new state from the reducer .
The intent is bound to the view controller through the bind function (that updates the view controller when the state is updated), and the main action (so to speak) is through the onNext()
function.
That onNext()
function requests the next state from the Reducer, and the logic here for presenting this new state has been separated out to a new function (presentNewState
) that introduces the newState
to the mainStore
- and it is separated because this is a task that would be reused in a larger project.
The role of the View Controller is to set up the Intent and bind the button to call the intent. In this implementation the intent is tightly coupled with the router, and there is certainly a call for using dependency injection but this theoretical small project has been set up with the following:
The MVI architecture is commonly in use in Android development, and is also in use where Redux is used in conjunction with React development.
This is a rather theoretical article that has looked at implementing such an architecture in Swift and iOS. If you are interested in separating the state from the logic of your App you might like to look at SwiftUI, and there are of course many other iOS architectures (MVC and MVVM are good places to start looking).
I hope you enjoyed reading this article, and it might have helped you in some way.
If you've any questions, comments or suggestions please hit me up on Twitter
Feel free to sign up to my newsletter