Working with Core Data

This guide provides details on how to work with Core Data and IGListKit.

Background

The main difference in the setup and architecture of a Core Data and IGListKit application is the configuration of the model layer. Core Data operates with a mutable model layer, where objects are always passed by reference and the same instance is modified when an object is edited.

IGListKit requires an immutable model in order to correctly calculate the diffing between model snapshots and to correctly animate the UICollectionView.

In order to satisfy these prerequisites, Core Data NSManagedObjects should not be used directly as ListDiffable objects. Instead, a view model (or some sort of token object) should be used to mimic (or act as a placeholder for) the data that will be displayed in the collection view.

Further discussion

There are further discussions on this topic at #460, #461, #407.

Basic Setup

The basic setup for Core Data and IGListKit is the same as the normal setup that is found in the Getting Started Guide. The main difference will be in the setup of the model used in the IGListAdapterDataSource.

Working with view model

Creating a view model

Suppose the Core Data model consist of:

extension User {
    @NSManaged var firstName: String
    @NSManaged var lastName: String
    @NSManaged var address: String
    @NSManaged var someVariableNotNeededInUI: String
}

A ViewModel object will contain only the necessary information needed to build UI. The properties of the ViewModel will be immutable:

class UserViewModel: NSObject {
    let firstName: String
    let lastName: String
    let address: String
}

We recommend writing a helper method to translate Core Data objects into ViewModel objects:

extension UserViewModel {
    static func fromCoreData(user: User) -> UserViewModel {
        // - Note: For avoiding Core Data threading violation, the following code should be wrapped in a
        // user.managedObjectContext?.performAndWait {}
        return UserViewModel(firstName: user.firstName, lastName: user.lastName, address: user.lastName)
    }
}

The IGListDiffable protocol is implemented on the ViewModel layer:

extension UserViewModel: ListDiffable {

    public func diffIdentifier() -> NSObjectProtocol {
        return NSString(string: firstName + lastName)
    }

    public func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let toObject = object as? UserViewModel else { return false }

        return self.firstName == toObject.firstName
            && self.lastName == toObject.lastName
            && self.address == toObject.address
    }
}

Setting up the view model in the adapter data source

Steps to configure the UICollectionView with the ViewModel:

  • Retrieve Core Data objects
  • Transform Core Data objects into ViewModel objects and return them
  • Track changes to Core Data objects and update the datasource with them

Retrieve Core Data objects

The way objects are retrieved from Core Data is depends on the project.

Example: Suppose there is a delegate Provider class with the role of fetching Core Data objects and checking for updates. It can use an NSFetchedResultsController to leverage on the Core Data framework and rely on automatic notifications for updates.

final class UserProvider: NSObject {

    private lazy var userFetchResultController: NSFetchedResultsController<User> = {
        let fetchRequest: NSFetchRequest<User> = NSFetchRequest(entityName: "User")

        // sort descriptors and predicates 
        // ...

        let fetchResultController = NSFetchedResultsController(
           fetchRequest: tripsFetchRequest,
           managedObjectContext: self.coreDataStack.mainQueueManagedObjectContext,
           sectionNameKeyPath: nil,
           cacheName: nil)

        // Set delegate to track CoreData changes
        fetchResultController.delegate = self

        return fetchResultController
    }()

    init(coreDataStack: CoreDataStack) {
        self.coreDataStack = coreDataStack
        super.init()
        do {
            try userFetchResultController.performFetch()
        }
        catch {
            fatalError("Cannot Fetch! \(error)")
        }
    }
}

Transform Core Data objects into view models

func getUsers() -> [UserViewModel]? {
    guard let users = self.userFetchResultController.fetchedObjects else { return nil }
    // Here we transform and return ViewModel objects!
    return users.flatMap { UserViewModel.fromCoreData(user: $0) }
}

Track changes to Core Data

The Provider will track changes to the Core Data model by listening to the NSFetchedResultsController methods and inform the application about this changes via KVO, notifications, delegation, etc.

extension UserProvider: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        self.delegate?.performUpdatesForCoreDataChange(animated: true)
    }
}

Configure the datasource

The data source retrieves ViewModels and configures the IGListSectionController with them:

func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return self.userProvider.getUsers()
}

Reacting to Core Data changes in UI

The UIViewController containing the UICollectionView, will react to the NSFetchedResultController messages by updating the UI:

func performUpdatesForCoreDataChange(animated: Bool) {
    // Updating contents of collection view
    self.adapter.performUpdates(animated: animated)
}