MVVM-2: A Deep Tour

G. Abhisek
Swift India
Published in
9 min readJun 4, 2018

Here we are back again with the second part of our MVVM series. If you are new to MVVM, you would love to read my first blog of this series, MVVM — 1: A General Discussion for a bit of clarity on MVVM. Why stay only with a simple demo of MVVM? As promised this will be a detailed tour in the MVVM park and thus a big fat blog ;). So hang on with me till the end.

What will you learn?

  1. Using MVVM in a real-time iOS application development scenario.
  2. Build up complex UI’s respecting MVVM concepts.
  3. Common encountered problems in MVVM and fixing them smartly.

EXCITED ….. !!!

Disadvantages of MVVM:

Before going to the main concepts lets revisit the common disadvantages of MVVM:

  • Managing view models and their state in nested views and complex UI’s is difficult.
  • Communication between various MVVM components and data binding can be painful.
  • Code reusability of views and view model is difficult.
  • MVVM for beginners is hard to put to use.

What are we gonna build?

We will build an app which will fetch your current location and show nearby places like ATMs, cafes, nightclubs, and restaurants around you, so if you wanna hang out, it would be really easy for you :). Didn’t get a better name than NEARBY.

Here is what the app will look like:

Basic thumb rules of MVVM:

  1. View Model is owned by the view and model is owned by the view model.
  2. View Model is responsible only for processing input to output and logic required for driving the UI.
  3. View Model should not modify the UI.
  4. The view should only be responsible for UI handling.
  5. The view should not interact with Data Model or vice-versa.

While building our app we will try to maintain these basic thumb rules of MVVM.

So without wasting time let’s hop into the arena.

Sample Code:

You can download the Sample Code and open up “Nearby” project under MVVM-2 folder.

Code Flow:

The project consists of three pages: Home, Places List and Place Detail page. For easy understanding, each of the pages has been considered as an individual module.

For this discussion, we will analyze the HomePage module. The other modules are designed similarly.

Home Page Module

Analysing the HomePage design, we can segregate the views in the page as home view, pagination cell and a table cell bearing a collection view. Furthermore, the pagination cell has a horizontal scroll view which has views showing a place detail.

If you think how to manage all the view models and segregating responsibilities between the components, you would find out MVVM makes the situation much worse especially for beginners.

There could be multiple approaches to build the below UI, but for our discussion lets segregate the view as per the below image:

Smells fishy ….. !!! Ya, for a beginner in MVVM the above segregation can be a blunder and time killer. So what are the problems that MVVM will cause:

  • Managing multiple nested views and view models.
  • View reusability with different business logic driving the UI. (Eg. A single CollectionTableCell used for multiple cells with different business logic.)
  • Communication between different components.

Our code architecture for view models, views, and models will be as follows:

Flowchart Description:

Our entire app and all its views are based on the above principle of segregation, so understanding this is quite important.

  • HomeView(i.e HomeViewController) owns the HomeViewModel(i.e HomeViewModel), so we have created a private property of it in the home controller. It helps us to make sure that no one could mess up with the view model except the owning view.
private var viewModel = HomeViewModel()
  • HomeViewModel, when informed of the view load or refresh action, gets the app data, configures the output and prepares the table data source array.
viewDidLoad = { [weak self] in
self?.getAppData(completion: {
self?.prepareTableDataSource()
self?.reloadTable()
})
}

Few pointers to note in HomeViewModel implementation:

  • We call the closure self.reloadTable() to inform HomeViewController to reload the table, thereby maintaining view’s responsibility to update the UI components.
  • App’s data fetching and data saving logic should be written in view model.
  • API request is made by the view model thereby conserving view models responsibility to perform the business logic.
  • As you can see, we have used closures for communication as they are small, can be moved around the code easily and many other benefits. But as Uncle Ben said, “With great power comes great responsibility.”, we should be utmost careful while using closures as they can be pretty messy and cause threading issues. There is a wonderful blog explaining why we should use callbacks instead of delegates.
  • The API that is used over here is Google Places API. You can read more about places API at Google Places API.
  • You would find an interesting approach that we have used to arrange our table data source array.
/// tableDataSource array definitionprivate var tableDataSource: [HomeTableCellType] = [HomeTableCellType]()
  • “tableDataSource” is a collection of “HomeTableCellType” enums. A particular HomeTableCellType is associated with its own viewModel.
/// Enum to distinguish different home cell types
enum HomeTableCellType {
case pagingCell(model: PaginationCellVM)
case categoriesCell(model: TableCollectionCellVMRepresentable)
case placesCell(model: TableCollectionCellVMRepresentable)
}

Wait …!! What is there with the TableCollectionCellVMRepresentable thing? That doesn’t seem to be a view model. Well, its an approach for simplifying code reusability of view that we will discuss later in this blog.

  • Since we know our number of cells and their types, it’s easy for us to configure our tableDataSource as below:
private func prepareTableDataSource() {
tableDataSource.append(cellTypeForPagingCell())
tableDataSource.append(cellTypeForCategoriesCell())
tableDataSource.append(contentsOf: cellTypeForPlaces())
numberOfRows = tableDataSource.count
}
  • Let us see an implementation of a cellType fetching method.
private func cellTypeForPlaces()->[HomeTableCellType] {
var cellTypes = [HomeTableCellType]()
let allPlaceTypes = PlaceType.allPlaceType()
for type in allPlaceTypes {
let topPlaces = Helper.getTopPlace(paceType: type, topPlacesCount: 3)
let placeCellVM = PlacesTableCollectionCellVM(dataModel: PlacesTableCollectionCellModel(places: topPlaces, title: type.homeCellTitleText()))
placeCellVM.cellSelected = { [weak self] indexPath in
self?.placeSelected(topPlaces[indexPath.item])
}
if topPlaces.count > 0 {
cellTypes.append(HomeTableCellType.placesCell(model: placeCellVM))
}
}
return cellTypes
}

What’s going on over here? :

For each and every place type we are having a table cell with an embedded collection view. So we iterate in all of our place types one by one and prepare the corresponding cell types with associated view models. Each sub-view models event is being observed in the HomeViewModel itself, and HomeViewModel thus takes necessary actions corresponding to it, thus maintaining our ownership hierarchy.

So how are we gonna use this dataSource in our tableView back in home page?

Let us check HomeViewController’s “func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)” :

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellType = viewModel.cellType(forIndex: indexPath)
switch cellType {
case .pagingCell(let model):
return cellForPagingCell(indexPath: indexPath, viewModel: model)
case .categoriesCell(model: let model):
return cellForCategoriesCell(indexPath: indexPath, viewModel: model)
case .placesCell(model: let model):
return cellForPlacesCell(indexPath: indexPath, viewModel: model)
}
}

What’s going on over here? :

We are fetching the cellType and iterating over them and asking respective methods to return cells corresponding to that indexPath passing the model as an argument.

Let us look at one of the cell fetching methods:

private func cellForCategoriesCell(indexPath: IndexPath, viewModel: CategoriesTableCollectionCellVM)->CollectionTableCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CollectionTableCell.reuseIdentifier, for: indexPath) as! CollectionTableCell
cell.prepareCell(viewModel: viewModel)
return cell
}

What’s going on over here? :

We first initialize a cell for categories and we prepare the cell providing it with a view model.

Hold…On!! How is the cell getting rendered…?

The cell gets rendered in the similar fashion as HomePage UI gets rendered via outputs from HomeViewModel. Having a quick glance on “CategoriesTableCollectionCellVM” init() method, we see:

init() {
prepareDataSource()
configureOutput()
}

What’s going on over here? :

We are first preparing the dataSource for the collectionView inside “CategoriesCell”. The data source is an array of “ImageAndLabelCollectionCellVM”, as collectionView’s dataSource will be an obvious collection of its cell view models. And then we configure the output of the view model, in this case, its “numberOfItems” and “title” properties.

And once the view model is configured we set up the UI in “CollectionTableCell” by calling its “setUpUI()” method.

Fancy….. Isn’t it? So as you can observe in the code, we have addressed the problems of managing nested views and their view models quite efficiently. The hierarchy of view models is similar to our hierarchy of UI’s thereby managing the dependency understanding quite easy.

Take Away Point:

  • The view model holds the state and the output of the UI.
  • The view models communicate with each other and then each view is informed of the output via their respective views.
  • The view is the sole owner of view model and view model is the sole owner of the data model.

Code Reusability of View:

Now we come to another hiccup that MVVM poses i.e Code Reusability. If you see our view segregation image, you would find that we have planned to use a single CollectionTableCell used for multiple cells i.e for showing categories and places which will tend to have different business logic. So how can we approach this?

A common solution would be going for if-else conditions depending upon the cell type and modify the business logic. But this again would make our view model bulky and if the View will be used at many other places, we will end up in a deep mess.

So why not using different view models for the same view?

Sounds complex?

Protocols are a wonderful way to make confirming classes implement a set of optional and required properties and methods. In our case, we would be creating a common protocol for each view model associated with a view.

As discussed earlier view model transforms input to output. So what view model provides to the view is data to be rendered on UI and callbacks for UI updation. Even though we have different business logic our output for a particular UI rendering is always the same.

Didn’t get it yet ….? Never mind let us take an example.

CollectionTableCell needs the following output, inputs, and events irrespective of any logic:

// Output for cell 
var title: String
var numberOfItems: Int
func viewModelForCell(indexPath: IndexPath) -> ImageAndLabelCollectionCellVM

//Input to viewModel informing that cell has been selected.
func cellSelected(indexPath: IndexPath)

// Event to provide callback selection of cells
var cellSelected: (IndexPath)->()

So why not create a protocol to contain all these and make each and every view models to conform to this protocol. So in code, we have prepared a TableCollectionCellVMRepresentable and two view models each for a cell to display places and category. Each view model confirms to TableCollectionCellVMRepresentable.

protocol TableCollectionCellVMRepresentable {
// Output
var title: String { get }
var numberOfItems: Int { get }
func viewModelForCell(indexPath: IndexPath) -> ImageAndLabelCollectionCellVM

//Input
func cellSelected(indexPath: IndexPath)

// Event
var cellSelected: (IndexPath)->() { get }
}

PlacesTableCollectionCellVM:

class PlacesTableCollectionCellVM: TableCollectionCellVMRepresentable {

var numberOfItems: Int = 0
var title: String = ""
var cellSelected: (IndexPath)->() = { _ in }
private var dataModel: PlacesTableCollectionCellModel!
private var dataSource: [ImageAndLabelCollectionCellVM] = [ImageAndLabelCollectionCellVM]()

init(dataModel: PlacesTableCollectionCellModel) {
self.dataModel = dataModel
prepareCollectionDataSource()
configureOutput()
}

private func configureOutput() {
title = dataModel.title
numberOfItems = dataSource.count
}

private func prepareCollectionDataSource() {
// Prepare the collection data source
{ ..... }
}

func viewModelForCell(indexPath: IndexPath) -> ImageAndLabelCollectionCellVM {
return dataSource[indexPath.row]
}

func cellSelected(indexPath: IndexPath) {
cellSelected(indexPath)
}

}

CategoriesTableCollectionCellVM:

class CategoriesTableCollectionCellVM: TableCollectionCellVMRepresentable {

// Output
var title: String = ""
var numberOfItems: Int = 0

// Events
var cellSelected: (IndexPath)->() = { _ in }

private var dataSource: [ImageAndLabelCollectionCellVM] = [ImageAndLabelCollectionCellVM]()

init() {
prepareDataSource()
configureOutput()
}

private func prepareDataSource() {
// Prepare the data source
{ .... }
}

private func configureOutput() {
title = "Want to be more specific"
numberOfItems = dataSource.count
}

func viewModelForCell(indexPath: IndexPath) -> ImageAndLabelCollectionCellVM {
return dataSource[indexPath.item]
}

func cellSelected(indexPath: IndexPath) {
cellSelected(indexPath)
}

}

So, we have two different view models with different logic to configure the outputs but with the same set of outputs and events.

What did we learn?

  1. How we can manage state and view model configuration for nested UI’s.
  2. Reused view with multiple view models.
  3. Understand MVVM in a real-time app building environment.

Sample Code: GitHub link

I would love to hear from you

You can reach me for any query, feedback or just want to have a discussion by the following channels:

Twitter — @gabhisek_dev

LinkedIn

Gmail: abhisekbunty94@gmail.com

Please feel free to share with your fellow developers.

--

--