Avatar of Matt Moriarity
Matt Moriarity

Getting Started with Combine and Core Data

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

Because Apple released SwiftUI and Combine together, you could be forgiven for ignoring Combine if you’re only using UIKit in your app. I certainly ignored it for a while, but I think Combine provides a huge opportunity to use better app architectures in your iOS app without feeling like UIKit is fighting you the entire time.

I want to demonstrate a progression I went through in my app for observing data changes from Core Data. I think that this shows the power that’s available in Combine if you’re willing to embrace it.

Core Data includes a very useful class called NSFetchedResultsController. You give it a fetch request and a delegate, and it will let the delegate know when there are changes to objects matched by that fetch request. It’s been around for years now, and was originally designed around UITableView’s API, so that you could accurately tell a table view which rows had been inserted or deleted. Now that I’m using diffable data sources, a lot of the functionality isn’t as useful to me anymore, but I still use it to know when to update my data source snapshots.

Initially, I was using a fetched results controller directly in my view controllers:

class ToDoItemsViewController: UITableViewController {
    enum Section: Hashable {
        case items
    }

    // ...
    private var fetchedResultsController: NSFetchedResultsController<ToDoItem>!

    override func viewDidLoad() {
        super.viewDidLoad()

        // not shown: create the diffable data source

        fetchedResultsController
            = NSFetchedResultsController(
                fetchRequest: ToDoItem.fetchRequest(),
                managedObjectContext: context,
                sectionNameKeyPath: nil,
                cacheName: nil
            )

        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch()
            updateSnapshot(animated: false)
        } catch {
            NSLog("Could not fetch to-do items: \(error)")
        }
    }

    private func updateSnapshot(animated: Bool = true) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, ToDoItem>()
        snapshot.appendSections([.items])
        snapshot.appendItems(fetchedResultsController.fetchedObjects ?? [], toSection: .items)
        dataSource.apply(snapshot, animatingDifferences: animated)
    }
}

extension ToDoItemsViewController: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        updateSnapshot()
    }
}

This is super useful! Now my snapshot gets updated any time rows are added or removed from the query results, so my app’s UI stays consistent as the user makes changes. I didn’t show it, but I also used the controller(_:didChange:at:for:newIndexPath:) delegate method to know when to update the content in existing cells.

Once I’d done this for a few different view controllers, the boilerplate got to me and I created a new abstraction called FetchedObjectList:

class FetchedObjectList<Object: NSManagedObject>: NSObject {
    let fetchedResultsController: NSFetchedResultsController<Object>
    let updateSnapshot: () -> Void
    let updateCell: (Object) -> Void

    init(
        fetchRequest: NSFetchRequest<Object>,
        managedObjectContext: NSManagedObjectContext,
        updateSnapshot: @escaping () -> Void,
        updateCell: @escaping (Object) -> Void
    ) {
        fetchedResultsController =
            NSFetchedResultsController(fetchRequest: fetchRequest,
                                       managedObjectContext: managedObjectContext,
                                       sectionNameKeyPath: nil,
                                       cacheName: nil)
        self.updateSnapshot = updateSnapshot
        self.updateCell = updateCell
        super.init()

        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch()
        } catch {
            NSLog("Error fetching objects: \(error)")
        }
    }

    var objects: [Object] {
        fetchedResultsController.fetchedObjects ?? []
    }
}

extension FetchedObjectList: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        updateSnapshot()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        updateCell(anObject as! ObjectType)
    }
}

All this object does is keep track of the current list of objects and listen for the fetched results controller delegate methods, which it then notifies the view controller about. Using it from the view controller looks like this:

class ToDoItemsViewController: UITableViewController {
    // ...

    private var toDoItemsList: FetchedObjectList<ToDoItem>!

    override func viewDidLoad() {
        super.viewDidLoad()

        // not shown: create the diffable data source

        toDoItemsList
            = FetchedObjectList(
                fetchRequest: ToDoItem.fetchRequest(),
                managedObjectContext: context,
                updateSnapshot: { [weak self] in
                    self?.updateSnapshot()
                },
                updateCell: { [weak self] toDoItem in
                    // look up the item's cell and update UI components from the new state
                }
            )

        updateSnapshot(animated: false)
    }

    private func updateSnapshot(animated: Bool = true) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, ToDoItem>()
        snapshot.appendSections([.items])
        snapshot.appendItems(fetchedResultsController.fetchedObjects ?? [], toSection: .items)
        dataSource.apply(snapshot, animatingDifferences: animated)
    }
}

This cuts down the boilerplate quite a bit, especially when repeating this pattern all over the app.

I’m going to give you a quick spoiler: FetchedObjectList hangs around for a lot of this story, but it definitely dies in the end.

Currently, my app is using Combine nearly everywhere it’s possible, but I didn’t start there. The adoption was gradual. At some point I started thinking about whether I could power my Core Data changes from it, so I tweaked FetchedObjectList to stop taking callbacks and instead expose publishers for those notifications:

class FetchedObjectList<Object: NSManagedObject>: NSObject {
    let fetchedResultsController: NSFetchedResultsController<Object>

    private let onContentChange = PassthroughSubject<(), Never>()
    private let onObjectChange = PassthroughSubject<Object, Never>()

    init(
        fetchRequest: NSFetchRequest<Object>,
        managedObjectContext: NSManagedObjectContext
    ) {
        fetchedResultsController =
            NSFetchedResultsController(fetchRequest: fetchRequest,
                                       managedObjectContext: managedObjectContext,
                                       sectionNameKeyPath: nil,
                                       cacheName: nil)
        super.init()

        fetchedResultsController.delegate = self

        do {
            try fetchedResultsController.performFetch()
        } catch {
            NSLog("Error fetching objects: \(error)")
        }
    }

    var objects: [Object] {
        fetchedResultsController.fetchedObjects ?? []
    }

    var contentDidChange: AnyPublisher<(), Never> {
        onContentChange.eraseToAnyPublisher()
    }

    var objectDidChange: AnyPublisher<Object, Never> {
        onObjectChange.eraseToAnyPublisher()
    }
}

extension FetchedObjectList: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        onContentChange.send()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        onObjectChange.send(anObject as! ObjectType)
    }
}

The shape of this version is pretty similar to what I had before. So what did I change?

  1. I’m no longer passing in callbacks in the initializer.
  2. I created a PassthroughSubject to replace each callback. A Subject in Combine can function as both a subscriber and a publisher, so it can both receive and publish objects. They’re often used like I’m using them here: as a bridge from imperative code to reactive code. A PassthroughSubject in particular is a simple pipe: when you send it objects, it sends them on to any active subscribers, then promptly forgets about them. A good subsitute for a callback.
  3. Instead of my fetched results controller delegate methods calling the callback functions, they now send equivalent messages to the appropriate subjects.
  4. Each subject is exposed via a property as a type-erased AnyPublisher. I don’t want code that is using a FetchedObjectList to try to send objects to my subjects, so I mask their real types so they are exposed only as publishers.

Now I can update my view controllers to use the new Combine-friendly fetched object lists:

private var cancellables = Set<AnyCancellable>()

override func viewDidLoad() {
    super.viewDidLoad()

    // ...

    toDoItemsList
        = FetchedObjectList(
            fetchRequest: ToDoItem.fetchRequest(),
            managedObjectContext: context
        )

    toDoItemsList.contentDidChange.sink { [weak self] in
        self?.updateSnapshot()
    }.store(in: &cancellables)

    toDoItemsList.objectDidChange.sink { [weak self] toDoItem in
        // find and update the cell
    }.store(in: &cancellables)

    // ...
}

Honestly, I’m not sure if this version is an improvement over what I had before. It’s a bit more verbose than the previous version, but I also now have a little extra complexity because I’m using Combine. Unlike the callbacks I used before, publishers don’t retain their subscribers, so Combine gives an AnyCancellable when you create a subscriber, which you need to keep a strong reference to somewhere or your subscription will be cancelled.

I’m handling this by keeping a set of cancellables on my view controller for all of my subscriptions, and using the store(in:) method on AnyCancellable to dump them all in there. When my view controller deinits, the set and all its contents will deinit as well and the subscriptions will be canceled. It works, but it’s definitely more management than what I was doing before.

So why did I do this? Well, this wasn’t the only part of my app that was starting to use Combine in interesting ways. I’d only just begun. The benefits of using Combine compound the more your app uses it, and eventually a little bit of cancellable management seems like a fine price to pay. In part two, I’ll show how I embraced Combine even further to create pipelines for the data in my app.