Articles, podcasts and news about Swift development, by John Sundell.

Different flavors of view models in Swift

Published on 30 Sep 2018

Most apps tend to be centered around a small number of core models. For example, a navigation app might have models such as Route and Destination, while a social networking app may deal with types like Friend, Post and Comment. But even though the data domain of an app might be relatively small, the way that data is used throughout the app tends to vary quite a lot depending on what view the data is being displayed in.

Because of that, we often find ourselves writing view-specific model logic - logic that essentially transforms a model into something that can be displayed to the user, or converts user input into a model update that is then propagated to other parts of the app.

View models, a concept popularized by Microsoft as part of the MVVM (Model-View-View Model) design pattern, attempts to make it easier to write and maintain such logic, by introducing dedicated types for it. The idea is that by putting view models in between our data models and the views that represent them, we can remove a lot of strong coupling between our views and models, and reduce the need for view controllers to contain model logic.

While it's completely possible to write apps for Apple's platforms using "pure" MVVM, this week, let's take a look at how we can use view models as a complement to the standard MVC design pattern - and how various flavors of view models can help us achieve different tasks.

Model transformations

Let's say that we're building an app that lets the user browse and buy books. One of our view controllers - BookDetailsViewController - is used to display details about a given book, and currently gets its information from an injected instance of Book (which is one of our core models):

class BookDetailsViewController: UIViewController {
    private let model: Book

    init(model: Book) {
        self.model = model
        super.init(nibName: nil, bundle: nil)
    }
}

However, we can't simply render all Book properties directly - some of them require logic specific to our details view, and some require transformations in order to be displayable. For example, here we combine the book's name with the name of its author to form the title of our view, and depending on the book's availability we render different subtitles:

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup title label
    let titleLabel = UILabel()
    titleLabel.text = "\(model.name), by \(model.author.name)"
    ...

    // Setup subtitle label
    let subtitleLabel = UILabel()

    if model.isAvailable {
        subtitleLabel.text = "Available now for \(model.price)"
    } else {
        subtitleLabel.text = "Coming soon"
    }

    ...
}

There's nothing really wrong with the above code from a UI perspective, but it'll both be really tricky to test (since we'd have to rely on the view controller's private subviews for verification), and our above viewDidLoad implementation could also become quite messy and hard to follow if we add a few more properties or conditions.

Instead, let's take a look at how we could encapsulate the above model transformation logic in a view model.

Read-only structs

Let's start with a simple view model flavor, that essentially gives our view controller a dedicated model to work with, by wrapping the data model it draws its information from in a view model - like this:

struct BookDetailsViewModel {
    private let model: Book

    init(model: Book) {
        self.model = model
    }
}

Note how the underlying Book model is kept private, which'll prevent our view controller from accessing it directly. Instead, the view controller will ask our new BookDetailsViewModel for all the information it needs using dedicated properties for each use case. Within each property implementation, we can then use the same model transformation logic that we previously performed in our view controller:

extension BookDetailsViewModel {
    var title: String {
        return "\(model.name), by \(model.author.name)"
    }

    var subtitle: String {
        guard model.isAvailable else {
            return "Coming soon"
        }

        return "Available now for \(model.price)"
    }
}

With the above in place, let's update our view controller to use our new view model instead of being given a Book model directly, which gives us a much simpler viewDidLoad implementation - since we can now directly display the data that the view model gives us:

class BookDetailsViewController: UIViewController {
    private let viewModel: BookDetailsViewModel

    init(viewModel: BookDetailsViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let titleLabel = UILabel()
        titleLabel.text = viewModel.title
        ...

        let subtitleLabel = UILabel()
        subtitleLabel.text = viewModel.subtitle
        ...
    }
}

The above struct-based view model flavor usually works great for view controllers that have a read-only relationship to their data. Even though introducing view models this way might seem like a small change, it can do wonders for separation of concerns and for testability - since we can now verify our model transformation logic completely in isolation, by writing unit tests against BookDetailsViewModel.

Performing updates

However, many view controllers also need to be able to update the data that they work with, so let's take a look at another view model flavor that enables us to do just that.

Here we have a BookEditorViewController that lets some of our users modify the details that in BookDetailsViewController were considered read-only. After the user has finished editing, we'd like to call a method that saves the updated data to the server, like this:

extension BookEditorViewController {
    func save(title: String, price: Price, isAvailable: Bool)  {
        // Send the updated book data to the server
    }
}

Again, this is logic that we could put inline in BookEditorViewController, but if we slightly modify our view model approach from before, a BookEditorViewModel would be a great place to encapsulate such logic - letting our view controller focus on what it does best - controlling views.

This time, we implement our view model using a class instead of a struct (since we want to be able to asynchronously mutate it), and initialize it with a BookSyncService as well as the underlying model to use:

class BookEditorViewModel {
    private var model: Book
    private let syncService: BookSyncService

    init(model: Book, syncService: BookSyncService) {
        self.model = model
        self.syncService = syncService
    }
}

To enable our view controller to update the underlying model and sync changes to our server, we'll add an API that takes a tuple containing the changes made and a closure to call once the update operation finished:

extension BookEditorViewModel {
    typealias Changes = (name: String, price: Price, isAvailable: Bool)

    func update(with changes: Changes,
                then handler: @escaping (Outcome) -> Void) {
        // Apply changes
        var updatedModel = model
        updatedModel.name = changes.name
        updatedModel.price = changes.price
        updatedModel.isAvailable = changes.isAvailable

        // Sync changes with the server
        syncService.sync(updatedModel) { [weak self] result in
            switch result {
            case .success(let newModel):
                self?.model = newModel
                handler(.success)
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
}

For more information about using tuples like we do above to group the changes together, check out "Using tuples as lightweight types in Swift".

Finally, let's update our editor view controller to use its new view model. Just like before we'll inject our view model instead of the underlying data model, and change things so that our view controller can get all the information it needs from its view model. We'll then implement the save method using the view model's update API, like this:

extension BookEditorViewController {
    func save(title: String, price: Price, isAvailable: Bool)  {
        let changes = (title, price, isAvailable)

        viewModel.update(with: changes) { [weak self] outcome in
            switch outcome {
            case .success:
                self?.showUpdateSuccessNotification()
            case .failure(let error):
                self?.showUpdateError(error)
            }
        }
    }
}

This flavor of view models is very similar to the idea of logic controllers - in that we use a dedicated type to encapsulate not only the way data is transformed, but also how it's saved or acted upon. Both techniques aim to solve the same problem, with the main difference being that a logic controller tends to always return a new state that the view controller then renders, while here the view controller will itself still pull the information it needs from its view model.

A two-way street

Let's take a look at one final view model flavor, which can be really useful when we need to implement two-way bindings - in that the view model can be updated by its view controller, and can also notify the view controller that it was externally updated as well.

Like many things in Swift, there's a ton of different ways that two-way bindings between a view controller and its view model can be implemented. We could use the observation pattern, notifications, functional reactive programming with frameworks like RxSwift, and a number of other techniques (all of which have their own merits) - but for this example we're going to use a simple closure.

Let's say that we're building a new feature for our book store app, that lets users review books. As a new review could come in as the user is looking at the reviews list, we'd like to make our list dynamically update when that happens. We'll start by implementing our view model, which in turn starts observing a BookReviewManager for updates once it's created, like this:

class BookReviewsViewModel {
    // The closure that will gets called every time the
    // view model was updated
    var updateHandler: () -> Void = {}

    private let book: Book
    private let manager: BookReviewManager
    private var reviews: [Book.Review]

    init(book: Book, manager: BookReviewManager) {
        self.book = book
        self.manager = manager
        self.reviews = manager.reviewsForBook(withID: book.id)

        startManagerObservation()
    }

    deinit {
        endManagerObservation()
    }
}

Then, whenever our BookReviewManager observation was triggered, we'll update the view model's local reviews data and call the update handler:

private extension BookReviewsViewModel {
    func startManagerObservation() {
        manager.addObserver(self, forBookWithID: book.id) {
            viewModel, reviews in
            viewModel.reviews = reviews
            viewModel.updateHandler()
        }
    }
}

For more information about implementing an observation API similar to the one we use above - check out "Observers in Swift".

Just like with our previous two view model flavors, our BookReviewsViewModel will contain view-specific properties that our view controller will read from in order to render its list of reviews, and will also include an API for adding a new review as well. All of this is done without exposing the underlying Book.Review data model at any point:

extension BookReviewsViewModel {
    var numberOfReviews: Int {
        return reviews.count
    }

    func titleForReview(at index: Int) -> String {
        let review = reviews[index]
        let stars = String(repeating: "⭐️", count: review.numberOfStars)
        return "\(stars) \"\(review.title)\""
    }

    func addReview(withTitle title: String,
                   text: String,
                   numberOfStars: Int) throws {
        let review = Review(
            title: title,
            text: text,
            numberOfStars: numberOfStars
        )

        try manager.add(review, forBookWithID: book.id)
    }
}

With the above in place, let's implement our view controller - starting with enabling automatic UI updates by binding our table view's reloadData method to our view model's updateHandler property, which will cause it to be called every time a review was added or removed:

class BookReviewsViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.updateHandler = tableView.reloadData
    }
}

Next, let's enable a new review to be added. Once the user hits the submit button, we'll send the user input to the view model using the addReview method (which'll throw in case the data is invalid):

extension BookReviewsViewController {
    func submitReview() throws {
        try viewModel.addReview(withTitle: titleView.text,
                                text: textView.text,
                                numberOfStars: starsView.value)
    }
}

Finally, we'll use our view model to implement UITableViewDataSource, asking it to provide the title and subtitle for each cell:

extension BookReviewsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int {
        return viewModel.numberOfReviews
    }

    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "review", for: indexPath)

        cell.textLabel?.text = viewModel.titleForReview(at: indexPath.row)
        cell.detailTextLabel?.text = viewModel.subtitleForReview(at: indexPath.row)

        return cell
    }
}

Above we let the view controller itself be the data source for its table view. Another option would be to implement a dedicated data source object (which could also be reused in other contexts). For more on that, check out "Reusable data sources in Swift".

We now have a continuously updating view controller, all without requiring it to know any specifics about the models that it renders - pretty cool! 👍

Conclusion

View models can be a really powerful tool in many different situations, since they let us put a layer between our view code and our model code, providing us with a dedicated place to put view-specific model transformation and update logic.

Just like when importing other concepts that Apple's SDKs weren't really designed for, there are many different ways that view models can be implemented in Swift - and each flavor comes with its own pros and cons.

What flavor that will be the right choice for any given project of course depends a lot on the requirements of that project, as well as the team's preferences. It may even be appropriate to use multiple flavors within the same code base, for example the simple struct-based flavor for read-only views - while one of the other, more powerful, flavors is used for dynamic ones.

What do you think? Do you currently use view models, or is it a concept that you'll try out? Let me know, along with any other questions, comments or feedback that you might have - on Twitter @johnsundell.

Thanks for reading! 🚀