New iOS Software Architecture: 4V Engine
Should you read this article?
This article is about a new software architecture which has more layers than VIPER and MVVM-C. It means that it may be more complex than other known architectures.
If you want to continue reading this article, you must accept my point of view that if we want a clean and testable architecture we should have several layers with just a responsibility.
I don’t want to sell it as the perfect architecture which can solve all your problems. It may not suit your needs perfectly. For this reason, I would suggest you to read this article thoroughly and judge by yourself if this architecture makes sense for your projects.
If you are wondering why MVC is not enough for iOS development, I would suggest you to jump in Why MVC is not enough.
Introduction
I created this blog writing an article about MVVM-C. Then, I wrote an article about SOLID principles. At this point, you may be thinking that I don’t practice what I preach. If I speak about “Single Responsibility Principle”, why do I want to use MVVM-C even if the Coordinator
layer has more than one responsibility? It creates the stack (View Model
, View
and Service
), adds View
in a parent UIKit
component and decides the routing adding/removing child coordinators. MVVM-C must be refactored a little bit to avoid breaking “Single Responsibility Principle”.
In this article, I will explain an alternative to the main iOS software architectures: 4V Engine.
Happy Reading!
Why MVC is not enough
I’ve already covered this point in a my previous article about MVVM-C but I want to write it again because I think it’s very important.
A common approach to become an iOS developer is looking at the documentation and following the patterns suggested by Apple to write some plain projects. It means that the majority of us have begun using MVC as software architecture to create our first applications. Step by step, we started to get used to MVC. At this point of the learning process, we think that MVC is the right way. It works, why should we move to another architecture which adds complexity to our code?
There are, mainly, two reasons:
- SOLID principles: The view controller has too many responsibilities.
- Testability: The view controller is difficult to test since it’s tightly coupled with
UIKit
.
If we want to solve the points above, we should move to another architecture. Unfortunately, nothing comes for free. This change has a cost: complexity. If we look at VIPER and MVVM-C, we can notice that there are several layers to manage and let communicate together. It may be overkilling if we have a plain application or we don’t really care of SOLID and testability.
My personal point of view is:
I like having a well-written code which follows the SOLID principles and properly tested to avoid bugs as much as possible. For this reason, I wanted to spend time and efforts to create a new clean software architecture.
Problems of MVVM-C
As said previously, MVVM-C is not enough. The Coordinator
layer breaks “Single Responsibility Principle” and must be split in three components with the following responsibilities:
- Manage the app routing.
- Create the stack (
View Model
,View
andService
). - Show
View
in a parentUIKit
component.
This idea of splitting the Coordinator
led me to 4V Engine.
I want to thank my friend Ennio Masi for pointing out this Coordinator
problem, which motivated me to find a solution.
Problems of VIPER
Another famous iOS architecture is VIPER. It has several layers which are very similar to MVVM-C with a different naming.
If we analyze VIPER, we can find the same problem of MVVM-C. The guilty layer is called Wireframe
and it’s the router of the architecture.
Let’s use, for the sake of the explanation, the following code copied from VIPER-SWIFT:
|
|
We can notice that presentAddInterfaceFromViewController
has too many responsabilities:
- Manage the routing.
- Create the
View
and its properties. - Show
View
in the parentUIViewController
.
We can notice that—with these three points—we have the same problems of the MVVM-C Coordinator
.
4V Engine
We’ve analyzed the common iOS software architectures and we have found some problems. If we are willing developers and we want to improve our code, we would need an alternative which refactors the previous ones. With this goal in mind, I created this new software architecture:
Don’t be afraid, it may be confusing but we are going through the explanation of each layer soon.
As we can see in the diagram above, the core of this architecture is made by View Presenter
, View Factory
, View
and View Model
, for this reason this architecture is called 4V Engine.
Getting Started
Now, it’s time to explain each single layer. Since I think that jumping in the code is the best way to learn something, we’ll use a sample app to cover each layer with an example.
You can find the Github repo here.
It’s a very plain application with two components:
- Users List: a users list fetched from a remote API.
- User Details: a view with the name of the user selected in the users list-we can select a user tapping the info button of a
UITableViewCell
in the users list.
Layers
I think the best way to explain the layers is starting from bottom (Model
) to top (Router
). Let’s start.
Model
The model represents the data of our application.
In our sample app, we have a model User
:
|
|
which represents the single user parsed from the API response.
Interactor
The Interactor
is the same used in VIPER.
This layer manages the Model
to prepare the data for the View Model
. The View Model
shouldn’t perform any operations directly on the model, but it should delegate Interactor
for any data manipulations.
In our sample app, we have an interactor which fetches the users from a remote API—thanks to the service HTTPClient
—and then transforms the JSON data received in an array of User
—thanks to the helper class UsersParser
—to be used inside our View Model
:
|
|
View Model
We can consider View Model
the most important layer of this architecture. Its responsibility is to interact with the UI to decide what to show and how to behave after an UI action.
This layer shouldn’t have any UIKit
references. If we want a communication between View
and View Model
, we should use an UI binding mechanism. I’ve already shown the main mechanisms in a my previous article. For this sample app, I’ve decided to avoid RxSwift for the binding since it would have increased the complexity of the examples. To keep everything as plain as possible, the binding has been achieved with the delegation pattern.
We can use View Model
with an its Interactor
to get the data to show in the UI, as we can see in the sample app:
|
|
With MVC, we are used to keep the business logic inside the view controller. With 4V Engine, we can move the business logic inside the View Model
and test it easily since we don’t have dependencies with UIKit
.
Note:
navigationDelegate
is used to communicate withRouter
. We’ll see it in Router.- The method
bind
is used for the UI binding betweenView
andView Model
. We’ll see it in View.
View
The View
layer represents any UIKit
components used to show something in the device screen.
In the sample app, the View
is a UIViewController
for the user details and a UITableViewController
for the users list.
The advantage of a good architecture is that we can test easily our layers. View
is usually the most difficult to test because it’s coupled with its dependency UIKit
. For this reason, we must keep this layer as plain as possible and move the business logic in a testable layer. The “testable” layer is the View Model
. As we’ve seen in View Model, the UI data is driven by the View Model
. In this way, we can move the business logic inside View Model
. View
becomes a dumb layer, which is used merely to show something in the device screen.
The important concept to understand with the View
is the UI binding, which allows us to set the communication between View Model
and View
. If you don’t know what is the UI Binding, please have a look at my previous article.
Here an example of View
from the sample app:
|
|
As we can see in this example, the UI binding is often bidirectional, some times we ask some data to View Model
and some times we get notified by View Model
to update the UI—like with usersListUpdated
.
Note:
The property viewModel
has the keyword unowned
. It’s required to avoid a retain cycle. Since View Factory
already keeps a strong reference of View Model
—as we’ll see in View Factory—View
doesn’t need to keep a strong reference of its View Model
.
View Factory
So far, the layers have been very similar to VIPER and MVVM-C. Now, it’s time to explain the layers which may be confusing at first glance.
The responsibility of View Factory
is creating the core of the architecture: View
, View Model
and Interactor
.
View Factory
alone may not make a lot of sense, we must see it in the right context. We’ll understand its usage in View Presenter.
Let’s see an example from the sample app:
|
|
Note:
- We are exposing
viewController
to be used inView Presenter
. We may also expose theView Model
for specific reasons. I think it can be private most of the time. The decision depends on what you have to achieve. - We are injecting
UsersListNavigationDelegate
inUsersListViewModel
to let theRouter
communicate with theView Model
in an abstract way. We’ll see the details of this delegate in Router.
View Presenter
The name of this layer may be a little bit confusing. We’re used to call Presenter
the layer which updates the View
—we have this layer in VIPER and MVP. In this architecture, the presenter is called View Model
and this layer is not a presenter but a View presenter. Keep reading to understand its responsibility.
The View Presenter
is the last piece of the puzzle of a component written with 4V Engine.
This layer has the responsibility to show the component in the device screen.
To achieve this goal, it must know what and where to show. The View
to add is provided by the View Factory
and the parent is injected from outside.
Let’s see an example from the sample app:
|
|
In this example, present()
is a very plain method to add a child view controller. If you have fancy UIViewController
transitions, this method is the right place to manage them.
You can notice that we are propagating UsersListNavigationDelegate
through the layers to use it in the View Model
. This is the downside of splitting the architecture in several layers.
Router
We’ve just finished to see the layers of a single component. At this point, we have a component almost ready to be shown in the screen. We need a last step: to decide when to show the component. This is the responsibility of Router
.
We usually have a Router
per story. In this context, my definition of story is:
The set of components which, together, define a flow in our application.
In our sample app, we have the story Users
which is the set of users list and user details together. Other stories can be:
- Onboarding: set of views to show how to use the application.
- Registration: set of views to create a new account, accept terms of use, validate email, …
- Items Purchase: set of views to show the basket, add delivery address, add card details for the payment, …
Let’s see how to use Router
for the story Users
in the sample app:
|
|
Note:
Router
has a dictionary with the presenters used. In this way, if we want to remove a component like inuserDetailsCloseDidTap
, we can easily get the right presenter using a key.
We can have a look at AppDelegate
to understand how to use the router:
|
|
Conclusion
I consider this article a presentation of the version 1.0.0
of 4V Engine. I changed it a lot of time and I’m sure that there is still room of improvement. For this reason, I would like some comments with your opinions, it would be greatly appreciated. Thank you.