How to successfully implement Model-View-ViewModel (MVVM) architecture pattern in iOS App development

Overcoming the drawbacks of MVP and MVC design patterns to deliver a superior product

Hsin
DBS Tech Blog
5 min readMay 17, 2022

--

Intro

Model-View-ViewModel (MVVM) has become increasingly popular in recent years, as more developers have realised that using Model-View-Controller (MVC) results in a challenge: massive view controllers. MVCs often mean that view controllers carry excessive responsibilities, leading to tight coupling and an increase in maintenance costs.

At DBS, we adopt MVVM in several consumer apps, such as PayLah! in Singapore and Card+ in Taiwan. In this post, I will be using Taiwan’s Card+ app as an example to highlight how we implemented MVVM.

The Importance of Architecture Patterns

Architecture Patterns are used widely in software development; applying a relevant architecture pattern makes software maintainable, manageable, testable, reusable, understandable, and flexible. It is crucial to ensure that the software we develop is maintainable, as we maintain our software for more than 80% of its lifetime. One of the ways to ensure easy maintenance is through the adoption of architecture patterns.

Main Concept of MVVM

MVVM binds user interfaces to Model classes through View Model. There are three main components of MVVM:

  • Model: A structure that manages data;
  • View: Displays visual elements and formatted data for users. It also receives user interactions, such as taps, clicks, and keyboard inputs; and
  • ViewModel: The mediator between Model and View. ViewModel retrieves raw data from Model and formats it into data that can be displayed to users (View)

Once a user triggers an action, the interaction between these three layers usually happens this way:

Figure 1: User triggers an event -> View/ViewController -> ViewModel -> Model -> View/ViewController

In MVVM, ViewModel formats data received from service and model types. After formatting data, ViewModel passes it to ViewController, which is then presented to the users. This data flow requires a callback mechanism to bind ViewModel and ViewController data, which we can achieve through some methods such as delegation and closure.

Data Binding: Delegation

Delegation is also known as delegate pattern, which is a design pattern that allows an object to communicate with its owner. Delegation broadly appears in Apple’s APIs, including UITableViewDelegate, CLLocationManagerDelegate, and UICollectionViewDelegate. The most common way to set up delegation is by using delegate protocols, which is what Apple’s APIs use.

  1. ViewModel

We define a protocol with a name that ends with -delegate, such as HomeViewModelDelegate. Inside HomeViewModelDelegate, we declare the method that ViewModel needs to call to update ViewController. In Figure2, we declare a method didFinishFetchingData inside HomeViewModelDelegate, which we would trigger when ViewModel finishes fetching the required data.

Next, we declare a property named homeViewModelDelegate and conform it to the HomeViewModelDelegate protocol in ViewModel. In Figure 2, we declare homeViewModelDelegate conforming to HomeViewModelDelegate.

Lastly, we call the delegate method by writing homeViewModelDelegate.methodName to update ViewController. In Figure 2, we call self.homeViewModelDelegate?.didFinishFetchingData(data) upon receiving the data. This is to notify the delegate that the data is ready to be used.

Figure 2: Delegate implementation example of ViewModel

2. ViewController

Now, we move on to ViewController, the delegate of ViewModel.

To receive updates from ViewModel, ViewController must assign itself as the delegate. In Figure 3, we assign self to self.viewModel.homeViewModelDelegate in the init function.

We then create an extension of ViewController to conform to HomeViewModelDelegate protocol, and implement the required methods that come with the protocol. In the example below, from the method didFinishFetchingData(_ message: String), we receive the message passed from ViewModel. We can also update the user interface (UI) upon receiving the data. This completes the whole delegation structure.

Figure 3: Delegate implementation example of ViewController

Data Binding: Closure

Implementing closure callbacks is relatively more flexible compared to delegations. Closures in Swift are blocks of codes that can be stored and passed as arguments to functions, frequently used in Apple SDKs (Software Development Kits) for event handling. Using closures as completion handlers is prevalent in many APIs.

Figure 4: Closure implementation example of ViewModel

Implementation requires the following steps:

  1. In ViewModel, declare a property that is closure type. In Figure 4, we declare a closure named didFinishFetchingData in ViewModel.
  2. Call this property when you need to update ViewController. In Figure 4, we call this property in fetchData once the data is ready to be passed to ViewController.
  3. In ViewController, access the property and assign a block of code to it, which will be executed once ViewModel triggers the callback method.

In Figure 5, in ViewDidLoad method, we assign a block of code to didFinishFetchingData where we obtain the message from ViewModel. We can do more tasks once the data is back, for example, updating the UI. This wraps up the implementation of closure callback.

Figure 5: Closure implementation example of ViewController

Closure VS Delegation: Which To Go For

When selecting the right mechanism for data binding, apart from matching the coding style with your team member, another key point is to check how often ViewModel needs to update ViewController.

If only one or two callbacks are required, I would go for closure. However, if ViewModel needs to update ViewController several times, I would go for delegation.

The reason is simple: having several callback closures scattered in the codebase decreases readability and control over the code. It is also costly to implement delegation, and it wouldn’t be cost-efficient if we were to use it for just one callback.

How We Leveraged MVVM In Card+

At DBS, we adopt MVVM and flow coordinator pattern in our Card+ app, which contains the three essential parts of MVVM — ViewController, ViewModel, Model, and flow coordinator. To make our ViewModel less complex, we separate networking and routing from it. Take one simple app page for example, if there’s just one screen, it would usually comprise ViewController, ViewModel, Model, and Service. The file tree usually looks like this:

Figure 6: The files tree for an app that has just one screen

Coordinator — Routing With Rxflow

RxFlow, a third-party navigation framework for iOS applications based on the Reactive Flow Coordinator pattern, manages all the screen navigations. We adopt RxFlow instead of the native Apple routing system as RxFlow increases dependency injection, testability, and UIViewController reusability. In addition, it is easy to manage navigation throughout the app as all the navigation cases are listed under one class. This involves four steps.

  1. Create an enum that inherits Step, where all screens are declared for navigation use. This enum covers all the possibilities we have for navigation. For example, if we have two screens in an app, declare an enum in this manner:
Figure 7: Declare one enum to involve all the steps for an app that uses two screens

2. Add in Flow, which defines a navigation area within the app. We also create a class that inherits Flow, where we initialise all ViewModels and ViewControllers within one method

Figure 8: Method for navigation

3. Instruct ViewModel to inherit Stepper and declare a property to store PublishRelay:

Figure 9: ViewModel implementation

4. Finally, when there is a need for navigation, we trigger our steps in the following manner:

Figure 10: Triggering Screen navigation

Conclusion

In Card+ app, we adopt MVVM with Flow Coordinator method to achieve dependency injection and increase code testability as well. With this architecture pattern, we can avoid the massive ViewController, which repeatedly exists in code using the Model-View-ViewController(MVVC) architecture pattern. Using a suitable architecture pattern also improves application maintenance.

Hsin Tzu Yen is an iOS developer at DBS Taiwan and works on Card+.

--

--

Hsin
DBS Tech Blog

iOS developer. Based in Taiwan. 👩🏽‍💻 🇹🇼