My MVVM Pattern Usage in iOS

The core concept of my application architecture

In the past few days, I have been developing a new company iOS project. Our company uses componential developing. I’m in charge of developing a wallet module including product check-stand and user wallet pages. After joining my company, this is my first native work, so I decided to use a more robust architecture in my module.

The Base MVVM

The MVVM design pattern has been prevalent recently—my application architecture is based on it. In the MVVM design pattern, the ViewModel replaces the view controller to handle our business logic. The view should keep high reusability, not reference any models, or import any business logic.

How to write a good view model is a question we need to think about carefully. What should a view model do? Requests data, parse data, handles business logic, and converts data to the model that views need.

Completely testable ViewModel

As mentioned, the view model is just another massive view controller if we let it do anything. We aim to test it in unit tests. Even offline, we should test all business logic in unit tests. So the network part doesn’t belong to the view model. The view model should reference a network layer and utilize it to fetch data.

Not only remote requests, sometimes the view model should save some data to local or write data to files. If we want to test these in unit tests, the view model cannot do them internally.

If an action will show an alert view, should we show an alert view directly in the view model by writing some UI logic codes? I don’t think it is a good idea because we cannot test it in unit tests.

All questions we asked show us that we can not do these in a view model. These functionalities should be from out.

Injecting All dependencies

The answer is injecting dependencies. We can inject the functionality the view models need into them.

If a view model needs to request user information, we can inject a data repository to the view model that provides a method to fetch user information.

If a view model wants to show an alert view, we can inject an alert responder that can display an alert view.

These repositories and responders will be declared as protocol. We have to inject these dependencies into the view model when initializing it.
All of the dependencies can be mocked when testing a view model.

Now we still have trouble that creating a view model needs too many parameters. Initializing a view controller needs to pass a parameter view model, but a view model maybe needs six parameters. If these parameters also need some parameters to create themselves, other employees to create your view controllers will be a disaster.

Using dependency containers or mediators to handle dependencies.

For hiding these adverse effects, we need a container to capture the creating process of our view controllers. We focus on processing these dependency relationships can keep other parts to be independent. We can create a dependency container for every view controller or a group of view controllers in the same business scope.

More light ViewModel

As you can see, our view models split the network, UI representation, and navigation to another middleware. The view models only process business logic now. A standard view model should directly output the data views needed, and this would cause the view model to bind to the specific view. If we want to do an A/B test, we need to create a new style view having different data input but the same business logic. We have to write a new view model for it.

We will discuss this problem below. First, let us see what the repository, responder, and navigation are.

Repository, Responder, Navigation

Repository

A repository is a data center that you can get data from it or save data via it. We don’t care how the repository processes our operations, return data from local or requesting remote, save data to memory, local disk or remote.

Maybe the data we require need to combine remote data and local data. The invokers(view model) only take care of what they want and don’t care how to fetch the data.

A standard repository usually contains two parts, Remote(requesting data from the server) and Storage(read/write data in local). The Remote only process network request, it’s an utterly reusable module. I suggest using PromiseKit to pack our network requests, and then we can use them easily when processing multi concurrency requests. These request functions should be functional. Their request params models and response models are declared into the Remote class.

The network layer data models are not business models.

Storage might contain some database operations, modifying UserDefaults, or something.

Responder

A view model not only needs to process data by a repository, but also needs to handle some UI and business logics. For examples, showing an alert or toast, recording events or communication with other view models. In the past, we usually write these code into view models, showing a toast by invoking a shared toast manager or a global method, writing statistic code into business methods directly.

All above scenes will break our view model’s dependency relationships. How do we display an alert in unit tests? Can you respond it? We cannot mock data for the methods implemented in view models.
We must remember that a view model can only reference business models directly, any other dependencies we rely on should be imported by dependency injection.

Responders are used to provide functionalities our view model needs. If you want to show an alert, you need to declare an AlertResponder protocol containing a method showAlert and set up it as your view model’s initialization function’s argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol AlertResponder {
func showAlert(message: String, actions: [String], callback: @escaping (Int) -> Void)
}

class ViewModel {
private let alertResponder: AlertResponder
init(alertResponder: AlertResponder) {
self.alertResponder = alertResponder
}

private func showAlert() {
alertResponder.showAlert("title", ["cancel", "ok"]) { index in
// Write your code in here.
}
}
}

Then we can mock a MockAlertResponder to import into the view model in unit tests and trigger the callback func of the alert responder manually by pure code.

Connecting ViewModels with Views

We have already learned how to write a standard view model. The next question is how to connect a view model to a view. It has to be noticed that a view cannot rely on any business model or view model. A view must be a pure UI render component. It contains some interaction functionalities but doesn’t care any business logics, so that we can reuse the view in other scenes.

In normal, we select using RxSwift or ReactiveCocoa to bind our view and view model. View models receive control events from views and then output signals that views can observe them to update UI.

In MVVM design pattern, we use view controllers to setup views layout and bind view model’s output signals to the views. The data in view model maybe not be appropriate to pass to views directly so that we should use map, filter and combineLatest, etc methods to transform value to suitable type.

I don’t think we should write data transformation processes into view controllers. We should keep view controller’s clarity. By convention, the best way is to place them into view models.

View Adapter

Sometimes, writing transformation code into view models will sacrifice some flexibility. For examples:

We have a view model outputting a coin amount value and we have a view that has a label displaying the coin number. We need to convert a number to a string. The string format isn’t constant. If the coin amount is 100000, we can display it as 100,000 or 100-000. If the view model executes the transformation process, we cannot reuse the view model when doing A/B test. The two different views have the same business logic but displaying different coin string format.

If the operation process cannot be put in view models or view controllers, we can only create a new middleware to cover it. this is the view adapter. The view model outputs a more generic value and the view does least process logics. The view adapter only contains some pure functions to convert data format. If we want ot do a A/B test, we can only create a new view adapter for the new view and reuse the view model straightly.

Communication Between ViewModels

First, every view models are independent. we cannot reuse one view model in another one. Usually, we use notification center, the Apple use it to process global event frequently. The EventBus is also a good choice.

MessageCenter/MessageSender/MessageReceiver

All above solutions are ok but we had better not use them in our view models directly like using basic component. If a view model want to publish a message to another one, the key of its action is to send a message. It doesn’t care who handle the message. A view model want to send a payment success message, it needs to declare a PaymentMessageSender protocol. The protocol requires a function sendPaymentSuccessMessage. Then we inject a payment message sender to our view model. Invoking its send message method to broadcast information when you want to notify others.

In the meantime, the view model that wants to observer some messages should declare a message receiver. The message receiver is injected into our view model and then we can observer event from it.

The way to connect message senders with receivers isn’t important. You can choice whatever you like.

Questions

The ViewModels need too many dependencies in its constructor.

As you can see, to create a view model needs many dependency injections. The major components need dependency injections and the sub components also need that.
The dependency injection management is quite complex, so we create a dependency container to manage them. We execute all the view model’s initializations in the dependency container. The container is just like a trash hiding all confused dependency relationships.

Should we reuse the view adapters?

one view for one view adapter.

Can we inject a view model to another?

I think it’s not a good idea.

Conclusion

In Swift, I think we have three ways to reuse a functionality, inheritance, extension and dependency injection.
The inheritance is easy to write, but hardly be tested and high coupling.
Easily using the extension is not good, but using the protocol extension (POP) is a good practice in Swift.
The dependency injection is the most flexible and can be tested completely, but writing dependency injections will cost more time to write more code. Developers need to determine how to split injection’s scopes. The most import is keeping, keeping your design pattern, keeping all things are injected.