In Part 1 of this series, I walked through the process of adding drag-and-drop functionality to a UICollectionView backed by Core Data. While I got it working, I mentioned that the solution, while functional, wasn’t as modern as it could be. The NSFetchedResultsControllerDelegate implementation, with its manual block operations, felt a bit clunky and error-prone. This post is about that additional refactoring: moving from the traditional NSFetchedResultsControllerDelegate to the more modern UICollectionViewDiffableDataSource.

Background

To recap, the PhotoListController was using a NSFetchedResultsController to manage photos from Core Data. The delegate methods were responsible for updating the UICollectionView in response to data changes. This involved a lot of manual work with BlockOperation to batch updates, which, as I discovered in Part 1, can be tricky to get right.

The goal of this refactoring was to simplify this process by leveraging UICollectionViewDiffableDataSource, which Apple introduced in iOS 13. Diffable data sources automatically handle the complexities of updating the UI, which means less code, fewer bugs, and a happier developer (me).

The Plan: A Modern Approach

The plan was to replace the existing UICollectionViewDataSource and NSFetchedResultsControllerDelegate methods with a UICollectionViewDiffableDataSource. Here’s how I broke it down.

Step 1: Laying the Foundation

First, I needed to define the section and item identifiers for the diffable data source. Since my collection view only has one section, this was straightforward. The item identifier would be the Photo object itself, because the NSManagedObject it inherits from already conforms to Hashable.

// In PhotoListController.swift

enum Section {
    case main
}

Step 2: Creating the Diffable Data Source

Next, I created a new property for the UICollectionViewDiffableDataSource in PhotoListController.swift.

// In PhotoListController.swift

private var dataSource: UICollectionViewDiffableDataSource<Section, Photo>!

Then, in viewDidLoad, I configured the data source. This involved creating the data source and providing a “cell provider” closure, which is responsible for configuring and returning a cell for a given IndexPath and Photo.

// In PhotoListController.swift, inside viewDidLoad()

dataSource = UICollectionViewDiffableDataSource<Section, Photo>(collectionView: collectionView!) {
    (collectionView: UICollectionView, indexPath: IndexPath, photo: Photo) -> UICollectionViewCell? in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCell.reuseIdentifer, for: indexPath) as! PhotoCell
    if let data = photo.thumbnailImage?.imageData as Data?, let image = UIImage(data: data) {
        cell.imageView.image = image
    }
    return cell
}

This code replaces the old collectionView(_:cellForItemAt:) method.

Step 3: Tying it to Core Data

This is where the magic happens. The NSFetchedResultsControllerDelegate methods were significantly simplified. Instead of manually managing BlockOperations, I just needed to create a new snapshot of the data and apply it to the data source in the controller(_:didChangeContentWith:) method.

// In PhotoListController+NSFetchedResultsControllerDelegate.swift

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
    guard let dataSource = self.dataSource else { return }

    var newSnapshot = NSDiffableDataSourceSnapshot<Section, Photo>()
    newSnapshot.appendSections([.main])
    newSnapshot.appendItems(controller.fetchedObjects as? [Photo] ?? [])

    dataSource.apply(newSnapshot, animatingDifferences: true)

    // Refresh empty state and other UI elements
    self.collectionView?.reloadEmptyDataSet()
    self.editButtonItem.isEnabled = controller.fetchedObjects?.count != 0
    if (controller.fetchedObjects == nil || controller.fetchedObjects?.count == 0) {
        self.setEditing(false, animated: true)
    }
}

With this change, I was able to remove the old controllerWillChangeContent, controller(_:didChange:at:for:newIndexPath:), and controllerDidChangeContent methods. A huge simplification!

Step 4: The Initial Load

Finally, I updated the initial data load in viewDidLoad. After performing the initial fetch, I just needed to call a helper function to update the data source with the fetched data.

// In PhotoListController.swift, inside viewDidLoad()

// Fetch data
do {
    try self.fetchedResultsController.performFetch()
    updateDataSource()
    self.editButtonItem.isEnabled = fetchedResultsController.fetchedObjects?.count != 0
} catch {
    fatalError("Error fetching photos: \(error)")
}

// new helper function
private func updateDataSource() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Photo>()
    snapshot.appendSections([.main])
    snapshot.appendItems(fetchedResultsController.fetchedObjects ?? [])
    dataSource.apply(snapshot, animatingDifferences: false)
}

The Result: Cleaner, Simpler Code

After the refactoring, the PhotoListController is much cleaner and easier to understand. The NSFetchedResultsControllerDelegate extension is now tiny, and I was able to delete a lot of the old, complex code for managing collection view updates.

The drag-and-drop functionality still works perfectly, but now it’s built on a much more robust and modern foundation.

Conclusion

This refactoring was a great investment. It not only modernized the codebase but also made it more resilient to bugs and helped me eliminate a ton of code and unit tests. If you’re still using the old NSFetchedResultsControllerDelegate with UICollectionView, I highly recommend looking into UICollectionViewDiffableDataSource. It’s a game-changer.

I hope this two-part series has been helpful. If you have any questions or feedback, feel free to reach out!