In the previous episode of Mastering Navigation With Coordinators, we refactored the PhotosViewController class. It now uses the MVVM pattern instead of the MVC pattern. We migrated the project from the MVC-C pattern to the MVVM-C or Model-View-ViewModel-Coordinator pattern.

We ran into a few issues that we need to tackle in this episode. Open PhotosViewController.swift and navigate to the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol. In the didBuy handler of the photo table view cell, we pass a Photo object to the didBuyPhoto handler of the photos view controller.

Because the PhotosViewController class no longer manages the array of Photo objects, it asks a PhotoViewModel object for its Photo object. This isn't ideal since the photos view controller can still directly access the Photo object of the PhotoViewModel object. That's a red flag if you're implementing the Model-View-ViewModel pattern. View controllers shouldn't have direct access to model objects.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: PhotoTableViewCell.reuseIdentifier, for: indexPath) as? PhotoTableViewCell else {
        fatalError("Unable to Dequeue Photo Table View Cell")
    }

    // Create View Model
    let viewModel = self.viewModel.photoViewModelForPhoto(at: indexPath.row)

    // Configure Cell
    cell.configure(with: viewModel)

    // Install Handler
    cell.didBuy = { [weak self] in
        self?.didBuyPhoto?(viewModel.photo)
    }

    return cell
}

We ran into a similar issue in the tableView(_:didSelectRowAt:) method of the UITableViewDelegate protocol. We pass a Photo object to the didSelectPhoto handler of the photos view controller. The photos view controller asks a PhotoViewModel object for its Photo object. This is another example of the photos view controller having direct access to the Photo object of the PhotoViewModel object.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)

    // Create View Model
    let viewModel = self.viewModel.photoViewModelForPhoto(at: indexPath.row)

    // Invoke Handler
    didSelectPhoto?(viewModel.photo)
}

Separation of Concerns

In the previous episode, we decided to adopt the Model-View-ViewModel pattern and that means that view controllers shouldn't have direct access to model objects. We need to respect the separation of concerns as defined by the Model-View-ViewModel pattern. The solution isn't complex. We delegate some of the tasks of the photos view controller to the photos view model.

Open PhotosViewController.swift on the left and PhotosViewModel.swift in the assistant editor on the right. We start by moving the didBuyPhoto and didSelectPhoto properties from the PhotosViewController class to the PhotosViewModel class.

import Foundation

class PhotosViewModel {

    // MARK: - Properties

    var didBuyPhoto: ((Photo) -> Void)?
    var didSelectPhoto: ((Photo) -> Void)?

    ...

}

This results in a few compiler errors. We resolve those in a few moments. The idea is simple. The photos view model acts as the bridge between the photos view controller and its coordinator, the photos coordinator in this example. This means that the photos view controller needs the ability to notify the photos view model when the user taps a buy button or a row in the table view. This is easy to implement. We need to add two methods to the PhotosViewModel class.

The first method is invoked when the user taps a buy button in the table view. We define a method, buyPhoto(at:), that accepts one argument of type Int. The argument informs the view model which photo the user is intending to buy. The photos view model uses the argument to select the Photo object from the array of Photo objects and passes it to the didBuyPhoto handler.

func buyPhoto(at index: Int) {
    didBuyPhoto?(dataSource[index])
}

The second method is invoked when the user taps a row in the table view. We define a method, selectPhoto(at:), that accepts one argument of type Int. The implementation is similar to that of the buyPhoto(at:) method. The photos view model uses the argument to select the Photo object from the array of Photo objects and passes it to the didSelectPhoto handler.

func selectPhoto(at index: Int) {
    didSelectPhoto?(dataSource[index])
}

We replace the didBuyPhoto and didSelectPhoto handlers in the PhotosViewController class with the methods we implemented in the PhotosViewModel class. In the tableView(_:cellForRowAt:) method, we invoke the buyPhoto(at:) method on the viewModel property, passing in the index of the row in the table view.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: PhotoTableViewCell.reuseIdentifier, for: indexPath) as? PhotoTableViewCell else {
        fatalError("Unable to Dequeue Photo Table View Cell")
    }

    // Create View Model
    let viewModel = self.viewModel.photoViewModelForPhoto(at: indexPath.row)

    // Configure Cell
    cell.configure(with: viewModel)

    // Install Handler
    cell.didBuy = { [weak self] in
        // Notify View Model
        self?.viewModel.buyPhoto(at: indexPath.row)
    }

    return cell
}

In the tableView(_:didSelectRowAt:) method, we no longer need to create a PhotoViewModel object. We invoke the selectPhoto(at:) method on the viewModel property, passing in the index of the row in the table view.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)

    // Notify View Model
    viewModel.selectPhoto(at: indexPath.row)
}

Refactoring the Photos Coordinator

The last piece of the puzzle is updating the implementation of the PhotosCoordinator class. Open PhotosCoordinator.swift and navigate to the showPhotos() method. We start by creating a PhotosViewModel instance. The didBuyPhoto and didSelectPhoto handlers are no longer properties of the PhotosViewController class. We replace photosViewController with viewModel to reflect that change. We assign the PhotosViewModel instance to the viewModel property of the photos view controller. That's it.

private func showPhotos() {
    // Initialize Photos View Model
    let viewModel = PhotosViewModel()

    // Install Handlers
    viewModel.didSelectPhoto = { [weak self] (photo) in
        self?.showPhoto(photo)
    }

    viewModel.didBuyPhoto = { [weak self] (photo) in
        self?.buyPhoto(photo, purchaseFlowType: .vertical)
    }

    // Initialize Photos View Controller
    let photosViewController = PhotosViewController.instantiate()

    // Configure Photos View Controller
    photosViewController.viewModel = viewModel

    // Install Handlers
    photosViewController.didSignIn = { [weak self] in
        self?.showSignIn()
    }

    // Push Photos View Controller Onto Navigation Stack
    navigationController.pushViewController(photosViewController, animated: true)
}

Build and run the application to make sure we didn't break anything. Tapping a row takes the user to the photo detail view. Tapping a buy button triggers the purchase flow.

Only the PhotosViewController class adopts the Model-View-ViewModel pattern. I leave it as an exercise to refactor the PhotoViewController and BuyViewController classes. Don't hesitate to reach out if you run into any issues.

What's Next?

The goal of the Model-View-ViewModel pattern is to put the view controllers of your project on a diet. A view controller should only be concerned with presenting data to the user and responding to user interaction. This means that view controllers shouldn't be interacting with model objects. That's the responsibility of the view model. The view model hands the view controller the data it needs to populate the user interface of its view.

This episode has shown that the same is true for user interaction. The photos view controller notifies its coordinator which photo the user has selected in the table view. It's tempting to expose the model object to the photos view controller to make that possible. I hope this episode has demonstrated that it takes very little effort to respect the separation of concerns of the Model-View-ViewModel pattern by delegating some of the view controller's tasks to its view model.