Earlier in this series, I mentioned that the coordinator pattern works well with the Model-View-ViewModel pattern. The resulting pattern is commonly referred to as MVVM-C or the Model-View-ViewModel-Coordinator pattern. In this episode, we refactor the PhotosViewController class. It currently uses the Model-View-Controller pattern. We update the implementation of the PhotosViewController class to use the Model-View-ViewModel pattern instead. I won't cover the details of the MVVM pattern in this episode, though. The MVVM pattern is covered in detail in Mastering MVVM With Swift.
Creating a View Model
Let's start by creating a view model for the PhotosViewController class. Create a group in the Photos View Controller group and name it View Models. Add a new Swift file to the group and name it PhotosViewModel.swift. Define a class with name PhotosViewModel.
import Foundation
class PhotosViewModel {
}
A view controller adopting the MVVM pattern shouldn't have direct access to model objects. This means that the PhotosViewController class shouldn't have direct access to the array of Photo objects. The view controller's view model is responsible for managing the array of Photo objects. Open PhotosViewController.swift in the assistant editor on the right and move the dataSource property to the PhotosViewModel class.
import Foundation
class PhotosViewModel {
// MARK: - Properties
private lazy var dataSource: [Photo] = [
Photo(id: "pli", title: "Misery Ridge", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg"), price: 25.99),
Photo(id: "jyg", title: "Grand Teton Sunset", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/2.jpg"), price: 15.99),
Photo(id: "rdz", title: "Orange Icebergs", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/3.jpg"), price: 27.99),
Photo(id: "aqs", title: "Grand Teton Sunrise", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/4.jpg"), price: 35.99),
Photo(id: "dfc", title: "Milky Tail", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/5.jpg"), price: 18.99),
Photo(id: "gbh", title: "White Sands National Monument", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/6.jpg"), price: 24.99),
Photo(id: "hnj", title: "Stonehenge Storm", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/7.jpg"), price: 25.99),
Photo(id: "jkr", title: "Mountain Sunrise", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/8.jpg"), price: 30.99),
Photo(id: "pah", title: "Colours of Middle Earth", url: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/9.jpg"), price: 50.99)
]
}
Before we continue, we define a variable property, viewModel, of type PhotosViewModel!. We declare the property as an implicitly unwrapped optional. If you don't like using implicitly unwrapped optionals, then you can declare the property as an optional. That's a personal choice.
import UIKit
class PhotosViewController: UIViewController, Storyboardable {
// MARK: - Properties
@IBOutlet var tableView: UITableView! {
didSet {
// Configure Table View
tableView.delegate = self
tableView.dataSource = self
}
}
// MARK: -
var viewModel: PhotosViewModel!
...
}
With the viewModel property in place, we can remove any references to the dataSource property in the PhotosViewController class. Let's start with the tableView(_:numberOfRowsInSection:) method of the UITableViewDataSource protocol. The view controller should ask the view model how many Photo objects the view model manages. In PhotosViewModel.swift, we define a computed property, numberOfPhotos, of type Int. As the name implies, the computed property returns the number of Photo objects in the dataSource property.
import Foundation
class PhotosViewModel {
// MARK: - Properties
private lazy var dataSource: [Photo] = [
...
]
// MARK: - Public API
var numberOfPhotos: Int {
return dataSource.count
}
}
With the numberOfPhotos computed property defined, it's easy to update the tableView(_:numberOfRowsInSection:) method of the PhotosViewController class.
extension PhotosViewController: UITableViewDataSource {
// MARK: - Table View Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfPhotos
}
...
}
The next method we need to update is the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol. We have a few options. The photos view controller could ask its view model for the Photo object that corresponds with the row, but that violates a core idea of the Model-View-ViewModel pattern. The view controller shouldn't have direct access to the model objects the view model manages. The option I prefer makes use of another view model. Let me explain how that works.
Creating Another View Model
Add a new Swift file to the View Models group and name it PhotoViewModel.swift. Define a struct with name PhotoViewModel.
import Foundation
struct PhotoViewModel {
}
Define a constant property, photo, of type Photo. We don't declare the photo property privately. I explain why in a moment. There's no need to implement an initializer since the PhotoViewModel struct automatically receives a memberwise initializer.
import Foundation
struct PhotoViewModel {
// MARK: - Properties
let photo: Photo
}
The next step is defining several computed properties. The computed properties expose the data other objects need. Open PhotosViewController.swift in the assistant editor on the right. The tableView(_:cellForRowAt:) method shows us which information we need to expose. We need to define three computed properties, title of type String, url of type URL?, and didBuyPhoto of type Bool. The implementation of the computed properties can be copied from the implementation of the tableView(_:cellForRowAt:) method.
import Foundation
struct PhotoViewModel {
// MARK: - Properties
let photo: Photo
// MARK: - Public API
var title: String {
return photo.title
}
var url: URL? {
return photo.url
}
var didBuyPhoto: Bool {
return UserDefaults.didBuy(photo)
}
}
You may be wondering which object is responsible for creating the PhotoViewModel objects the photos view controller needs to populate its table view. We put the PhotosViewModel class in charge of that task.
Open PhotosViewModel.swift. We need to implement a method that allows the photos view controller to ask for a PhotoViewModel object for each row of the table view. We name the method photoViewModelForPhoto(at:). It accepts one argument of type Int. The implementation is straightforward. We fetch the Photo object that corresponds with the index that is passed to the method and use it to create a PhotoViewModel instance.
import Foundation
class PhotosViewModel {
// MARK: - Properties
private lazy var dataSource: [Photo] = [
...
]
// MARK: - Public API
var numberOfPhotos: Int {
return dataSource.count
}
func photoViewModelForPhoto(at index: Int) -> PhotoViewModel {
return PhotoViewModel(photo: dataSource[index])
}
}
Populating the Table View
It's time to update the implementation of the tableView(_:cellForRowAt:) method. We ask the view model for a PhotoViewModel object by invoking the photoViewModelForPhoto(at:) method. We could use the view model to configure the PhotoTableViewCell instance, but I'd like to take it one step further. We already spent time creating the PhotoViewModel struct. I'd like to use it more efficiently. The table view cell should be able to configure itself if we hand it a PhotoViewModel object. This is the API I'd like to end up with.
// Create View Model
let viewModel = self.viewModel.photoViewModelForPhoto(at: indexPath.row)
// Configure Cell
cell.configure(with: viewModel)
The pattern I usually use looks like this. We first create a protocol. Add a new Swift file to the Protocols group and name it PhotoPresentable.swift. Define a protocol with name PhotoPresentable.
import Foundation
protocol PhotoPresentable {
}
The interface of the PhotoPresentable protocol mimics that of the PhotoViewModel struct. We define three properties, title of type String, url of type URL?, and didBuyPhoto of type Bool.
import Foundation
protocol PhotoPresentable {
// MARK: - Properties
var title: String { get }
var url: URL? { get }
// MARK: -
var didBuyPhoto: Bool { get }
}
Revisit PhotoViewModel.swift. We create an extension for the PhotoViewModel struct and use it to conform PhotoViewModel to the PhotoPresentable protocol. We don't need to implement any of the properties of the PhotoPresentable protocol because the PhotoViewModel struct implicitly conforms to the PhotoPresentable protocol.
import Foundation
struct PhotoViewModel {
// MARK: - Properties
let photo: Photo
// MARK: - Public API
var title: String {
return photo.title
}
var url: URL? {
return photo.url
}
var didBuyPhoto: Bool {
return UserDefaults.didBuy(photo)
}
}
extension PhotoViewModel: PhotoPresentable {}
Configuring a Table View Cell
With the PhotoViewModel struct and the PhotoPresentable protocol in place, we can refactor the configure(title:url:didBuyPhoto:) method of the PhotoTableViewCell class. You may be wondering why we defined the PhotoPresentable protocol? The PhotoTableViewCell class shouldn't need to know about the PhotoViewModel struct. It only needs an object that provides the data it needs to configure itself. The PhotoPresentable protocol adds a layer of abstraction between the PhotoViewModel struct and the PhotoTableViewCell class.
Open PhotoTableViewCell.swift and navigate to the configure(title:url:didBuyPhoto:) method. We first update the method definition. It accepts a single argument of type PhotoPresentable. The table view cell uses the PhotoPresentable object to populate its labels and its image view.
// MARK: - Public API
func configure(with presentable: PhotoPresentable) {
// Configure Title Label
titleLabel.text = presentable.title
// Show/Hide Buy Button
buyButton.isHidden = presentable.didBuyPhoto
// Animate Activity Indicator View
activityIndicatorView.startAnimating()
guard let url = presentable.url else {
return
}
// Create and Resume Data Task
dataTask = URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
guard let data = data else {
return
}
// Create Image
let image = UIImage(data: data)
DispatchQueue.main.async {
// Update Thumbnail Image View
self?.thumbnailImageView.image = image
}
}
// Resume Data Task
dataTask?.resume()
}
Handling Interaction
Revisit PhotosViewController.swift and navigate to the tableView(_:cellForRowAt:) method. The closure assigned to the didBuy handler of the table view cell still references a Photo object. Remember that the view controller shouldn't have direct access to model objects. We can work around this problem by asking the PhotoViewModel object for its Photo object. That is why we haven't declared the photo property of the PhotoViewModel struct privately. This solution isn't ideal since the view controller can directly access the Photo object. We resolve this issue later.
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 need to make a similar change in the tableView(_:didSelectRowAt:) method. We create a PhotoViewModel object by invoking the photoViewModelForPhoto(at:) method of the PhotosViewModel class. We ask the photo view model for its Photo object and pass it to the didSelectPhoto handler of the view controller. This is a temporary workaround. We resolve this issue later.
// MARK: - Table View Delegate
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)
}
Injecting the View Model
Before we can run the application, we need to inject a PhotosViewModel instance into the photos view controller. You learned about dependency injection and the coordinator pattern earlier in this series.
One of the most common questions developers have about the Model-View-ViewModel pattern is "Which object is responsible for creating the view model of a view controller?" The view controller shouldn't be responsible for creating its view model. That's a pattern you should try to avoid. This means that another view controller is usually responsible for creating and injecting the view model.
The solution is much simpler if you use the coordinator pattern. The coordinator is responsible for creating and configuring the view controller. This also means that the coordinator is the ideal candidate to create and inject the view model into the view controller. Let me show you how that works.
Open PhotosCoordinator.swift and navigate to the showPhotos() method. We use property injection to inject the PhotosViewModel instance into the photos view controller. The change we need to make is simple. We create a PhotosViewModel instance and assign it to the view controller's viewModel property. It's that simple.
// MARK: - Helper Methods
private func showPhotos() {
// Initialize Photos View Controller
let photosViewController = PhotosViewController.instantiate()
// Configure Photos View Controller
photosViewController.viewModel = PhotosViewModel()
// Install Handlers
photosViewController.didSignIn = { [weak self] in
self?.showSignIn()
}
photosViewController.didSelectPhoto = { [weak self] (photo) in
self?.showPhoto(photo)
}
photosViewController.didBuyPhoto = { [weak self] (photo) in
self?.buyPhoto(photo, purchaseFlowType: .vertical)
}
// Push Photos View Controller Onto Navigation Stack
navigationController.pushViewController(photosViewController, animated: true)
}
Build and run the application. The application hasn't changed functionally or visually. The changes we made are invisible to the user. The PhotosViewController class now uses the MVVM pattern instead of the MVC pattern. We have migrated the project from the MVC-C pattern to the MVVM-C or Model-View-ViewModel-Coordinator pattern.
What's Next?
In the next episode, we implement solutions for the workarounds in the PhotosViewController class. If a project adopts the MVVM pattern, then view controllers shouldn't have direct access to model objects. We address that in the next episode.