Avatar of Matt Moriarity
Matt Moriarity

Adopting MVVM with Combine and Core Data

This article is part of a series on using Combine to observe Core Data changes:

In the previous post, I ended up with a Combine publisher for the snapshots of data that my table view displays, so my table view was powered by a data pipeline. This pipeline lives within my view controller class, but I actually think Combine really shines when you start using it to unwind the Massive View Controller problem that is endemic to UIKit.

UIViewControllers tend to have a lot of responsibilities, and while they do have “controller” in their name, from an MVC perspective they are inherently stuck interacting with view-layer concerns just by how UIKit is structured. It’s best to let them keep those responsibilities and find ways to extract the logic that controls the flow of data through your app into another place. I think the model-view-viewmodel (MVVM) architecture does this well with the concept of ViewModels, especially when using Combine.

Briefly, a view model is a sort of translation layer between the domain model of the app (the Core Data objects and methods in this case) and the view layer that displays UI to the user. A view model will expose information about the model and about the state of the app in a way that is useful to the view. This allows the view to not concern itself with logic about app data and app state: it just presents what the view model exposes, and tells the view model when the user does things that need to affect app state.

The way I’ve been implementing it, each view controller has a corresponding view model that it owns and binds to. Using Combine, the view model can expose publishers for data that is useful to the view, and the view controller can subscribe to those publishers to keep its UI up-to-date.

Credit: Michał Cichecki for publishing a sample app showing how you might use Combine, UIKit, and MVVM together. It inspired to me to explore this path within my own app.

Splitting things up like this in my current example might look like this:

class ToDoItemsViewModel {
    private let itemsList: FetchedObjectList<ToDoItem>
    
    init() {
        itemsList = FetchedObjectList(/* ... */)
    }
    
    typealias Snapshot = NSDiffableDataSourceSnapshot<Section, ToDoItem>

    var snapshot: AnyPublisher<Snapshot, Never> {
        itemsList.objects.map { toDoItems in
            var snapshot = Snapshot()
            snapshot.appendSections([.items])
            snapshot.appendItems(toDoItems, toSection: .items)
            return snapshot
        }.eraseToAnyPublisher()
    }
}

class ToDoItemsViewController: UITableViewController {
    let viewModel: ToDoItemsViewModel

    // I encourage you to make a superclass for these rather than declare them
    // in every view controller.
    var cancellables = Set<AnyCancellable>()
    @Published var animate = false

    // Create the view model when loading the controller from the storyboard
    required init?(coder: NSCoder) {
        viewModel = ToDoItemsViewModel()
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // ...create the data source...

        viewModel.snapshot.apply(to: dataSource, animate: $animate)
            .store(in: &cancellables)

        // ...more subscriptions...
    }
}

The rewards for this separation get bigger as the complexity of the app grows. One thing I want to point out is that the view model is extremely testable. The publishers don’t care what is subscribed to them, so you can bind your view model to a fake view class and test the effects of different changes without creating any UI at all.

View models are also easier to nest than view controllers. The cells for my to-do items are likely complex enough to deserve their own view model class: ToDoItemCellViewModel. I’d like to have an instance of this class for each cell in my table. It will expose publishers and actions for a specific to-do item, and the UITableViewCell subclass will bind to it.

class ToDoItemCellViewModel {
    let item: ToDoItem

    init(item: ToDoItem) {
        self.item = item
    }

    var text: AnyPublisher<String, Never> {
        item.publisher(for: \.text).eraseToAnyPublisher()
    }

    var isChecked: AnyPublisher<Bool, Never> {
        item.publisher(for: \.isChecked).eraseToAnyPublisher()
    }

    func toggleChecked() {
        item.isChecked.toggle()
    }
}

class ToDoItemTableViewCell: UITableViewCell {
    var cancellables: Set<AnyCancellable>()
    var viewModel: ToDoItemCellViewModel?
    
    @IBOutlet var textLabel: UILabel!
    @IBOutlet var checkedButton: UIButton!

    func bind(to viewModel: ToDoItemCellViewModel) {
        self.viewModel = viewModel

        viewModel.text.assign(to: \.text, on: textLabel)
            .store(in: &cancellables)

        viewModel.isChecked
            .map { checked in checked ? "Checkbox_Checked" : "Checkbox" }
            .map { name in UIImage(named: name) }
            .sink { [checkedButton] image in
                checkedButton?.setImage(image, for: .normal)
            }.store(in: &cancellables)
    }

    @IBAction func toggleChecked() {
        viewModel?.toggleChecked()
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        cancellables.removeAll()
    }
}

Awesome, but how do I actually keep track of these view models and wire them up to the cells? I think the easiest way to do this is to use the cell view models themselves as the items in your diffable data source snapshots. It makes it easy for your view controller to get the model for an index path, and you don’t have to do extra bookkeeping to map between IDs or model objects and the view model instances.

But if I do that, I have a new problem: how do I keep a consistent list of view models in response to changes in my Core Data objects? I’d like to avoid recreating cell view models unnecessarily: ideally I would only create a view model the first time a particular to-do item appeared in the list, and then I would keep reusing that view model as long as it was there.

It turns out that a stream of lists is not really the right model for this. Instead, what I’d really like is a stream where each item is a collection of changes to the list of objects. With this, I can observe when to-do items are added and removed, and I can add or remove cell view models as needed from an array that I maintain in the parent view model.

Before I create a publisher to provide this to me, let’s see what it would look like to use a hypothetical one. I’m taking advantage of some new APIs in the Swift 5.1 standard library to support diffing two collections and returning a collection of the differences between them.

class ToDoItemsViewModel {
    @Published private(set) var itemViewModels: [ToDoItemCellViewModel] = []

    var itemChanges: AnyPublisher<CollectionDifference<ToDoItem>, Never> {
        // ...
    }

    init() {
        $itemViewModels.zip(itemChanges) { existingModels, changes in
            var newModels = existingModels
            for change in changes {
                switch change {
                case .remove(let offset, _, _):
                    newModels.remove(at: offset)
                case .insert(let offset, let toDoItem, _):
                    let model = ToDoItemCellViewModel(item: toDoItem)
                    newModels.insert(transformed, at: offset)
                }
            }
            return newModels
        }.assign(to: \.itemViewModels, on: self).store(in: &cancellables)
    }
}

To keep the list of view models up-to-date, I start with an empty list of them in a @Published property. I use zip with a transform closure to create a new publisher that combines the current list of view models with the next list of to-do item changes to produce a new list of view models. I use assign to store this new list of view models back into my published property. This should cause it to publish a new value, which will then get combined with the next list of changes from itemChanges and keep the cycle going.

To make it more concrete, let’s look at an example of how things would flow through these streams:

  1. $itemViewModels receives a subscription and publishes its initial value, the empty array. For now, nothing happens in our zipped publisher, because itemChanges hasn’t published a value.
  2. itemChanges receives a subscription and publishes an initial list of collection changes to populate the list of items from an empty state.
  3. zip pairs these two values and calls the transformer. It sees a bunch of .inserts in the list of changes and creates view models for each to-do item that was inserted.

    zip looks kind of like combineLatest and has a similar signature, but it works differently. zip is named that because it works kind of like a zipper: it pairs up elements from two streams, one-by-one. If one stream publishes a value before the other has one ready to go, it will wait and publish nothing until the other stream has something to pair with.

    I need that here, because I never want to replay a set of changes onto a list of view models that has already been updated.

  4. This new list of view models is published and assigned to itemViewModels. This list is published as a new value on $itemViewModels, and the zip waits for a new list of changes from itemChanges.
  5. Now, say the user removes an item from the list. When they do, itemChanges will publish a list with a single .remove change.
  6. The zip will now publish the pair of current view models and this new change. The transformer will remove the corresponding view model from the list and publish the new list.
  7. That new list gets assigned back to itemViewModels, and the cycle continues from (4) for any subsequent insertions and removals.

So now I’ve shown that if I can get a publisher to give me a stream of changes to my list of model objects, I can keep a consistent list of view models for them. But currently, that publisher doesn’t exist. Thankfully, nothing is stopping me from writing one myself, so in the final part of this series, that’s exactly what I’ll do.