Iglistkit: [Core Data] update cell on model update event?

Created on 2 Feb 2017  路  12Comments  路  Source: Instagram/IGListKit

New issue checklist

General information

  • IGListKit version:
  • iOS version(s):
  • CocoaPods/Carthage version:
  • Xcode version:
  • Devices/Simulators affected:
  • Reproducible in the demo project? (Yes/No):
  • Related issues:

It is a question and request.
In this question

407

I could find answer on first part of my question.
Here it is.
How could CoreData objects be integrated with IGListKit especially without NSFRC updates delegate methods?

I want to update on model updates.

From example:

extension User: IGListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return userId // yes, ok. primary key.
  }

  func isEqual(toDiffableObject object: Any?) -> Bool {
    if let object = object as? User {
      return self.shouldBeUpdatedBy(object) ;// well, I want to use core data updates.
    }
    return false
  }

  func shouldBeUpdatedBy(object: User) {
     return self.diffIdentifier() == object.diffIdentifier()
  }
}
question

All 12 comments

Hi @lolgear!

I use IGListKit with CoreData.
and to check for CoreData updates I use a NSFetchedResultsController.

I create it in the IGListKitAdapterDataSource and I use
FetchResultController.fetchedObjects
in the datasource methods to get the objects I need to display.

I then use the NSFRC methods to update IGListKit:

NSFetchedResultsControllerDelegate {
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.adapter.performUpdates(animated: animated)  
}
}

However I am now having a issue with performUpdates that I reported in #460
because when CoreData objects are updated, even calling performUpdates does not reflect in UI correctly (even if the datasource is correctly updated)

Hey @lolgear! You might want to check out my answer on #460 to see if that helps. Core Data and detecting changes with IGListKit is a little tricky.

@rnystrom
@racer1988
so the best way is to build view model over model and monitor updates in it? ( one of the implementations ).
transfer updates part to something that could be immutable and could be diffable in its nature?
At the end my model should be something like:

class UserContactsViewModel {
    let name : String
    let phoneNumber : PhoneNumber
    let userId : String
    let primaryKey : String
    // dynamic updates based on dataSource?
}

extension UserContactsViewModel : NSCopying {
    // could copy view model in case of simple structure.
}

extension UserContactsViewModel : IGListDiffable {
    func diffIdentifier() -> NSObjectProtocol {
        return userId // yes, ok. primary key.
    }

    func isEqual(toDiffableObject object: Any?) -> Bool {
        if let object = object as? User {
            return self.diffIdentifier == object.diffIdentifier
        }
        return false
    }
}

@lolgear I would probably create an almost mirror copy of your core data model and add some helper functions to make it easier to init. I'd also stick w/ @racer1988's idea of using NSFetchedResultsController, it's solid at responding to changes among a MOC stack.

It might be more code (sort of duping the models) but you will end up with so much more control over your app's behavior instead of fighting Core Data's mutability.

class User: NSManagedObject {
  var name: String?
  var birthday: Date?
}

class ListUser: NSObject {
  let name: String
  let birthday: Date

  static func fromMOC(user: User) -> ListUser? {
    guard let name = user.name, let birthday = user.birthday 
      else { return nil }
    return ListUser(name: name, birthday: birthday)
  }
}

// IGListAdapterDataSource

func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] {
  return self.coreDataUsers.flatMap({ ListUser.fromMOC($0) })
}

// NSFRC Notification

func nsfrcObjectsChanged(notification: NSNotification) {
  self.adapter.performUpdates(animated: true)
}

@rnystrom

now I am confused with

// NSFRC Notification

func nsfrcObjectsChanged(notification: NSNotification) {
  self.adapter.performUpdates(animated: true)
}

Do you mention
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
method?

In your example IGListDiffable protocol adoption omitted. Is it correct for ListUser object to don't have this extension? ( Some conventions in IGListKit like NSObject is diffable by default? )

Hi @lolgear,

in BOLD the answer to your questions

Model

Your model is made of NSManagedObject

class User: NSManagedObject {
  var name: String?
  var birthday: Date?
}

ViewModel

Your "ViewModel" is made of NSObjects.

class ListUser: NSObject {
  let name: String
  let birthday: Date

  static func fromMOC(user: User) -> ListUser? {
    guard let name = user.name, let birthday = user.birthday 
      else { return nil }
    return ListUser(name: name, birthday: birthday)
  }
}

Above snippet from @rnystrom supposed you have the following code in your repo:

https://github.com/racer1988/IGListKitPerformUpdates/blob/master/Marslink/Models/NSObject%2BIGListDiffable.swift

this way, all NSObjects will be IGListDiffable.
(for trivia: in IGListKit 1.0 this was part of the framework, it was removed in 2.0)

import IGListKit

// MARK: - IGListDiffable
extension NSObject: IGListDiffable {

  public func diffIdentifier() -> NSObjectProtocol {
    return self
  }

  public func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
    return isEqual(object)
  }

}

Datasource

The datasource is the object that asks CoreData for objects, transforms them into IGListDiffable ModelView objects and returns them to the adapter/collectionView.

func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] {
  return self.fetchResultController.fetchedObjects.flatMap({ ListUser.fromMOC($0) })
}

It also uses the NSFetchedResultController delegate to monitor changes to CoreData, and reflects them in UI

  • Datasource contains a NSFetchResultController

var fetchResultController: NSFetchedResultsController<Stuff>

  • Datasource is also delegate for the NSFRC:

fetchResultController.delegate = self

  • Datasource implements the delegate code:
extension DataSource: NSFetchedResultsControllerDelegate {
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    self.adapter.performUpdates(animated: true)
  }

Advanced ideas and best practices

It is a good approach/architecture to split all this code in different classes.
So you will have a separate class for Controller, DataSource, CoreDataProvider etc..
splitting them into different classes requires that you use delegation or notification to communicate between them.

So if your DataSource is the NSFetchedResultControllerDelegate, it needs to tell the controller (which owns the Adapter) that there is a need to call performUpdates.
This can be done in a lot of ways.

The one suggested by @rnystrom in his example was to raise a notification in the datasource
and catch it in the controller with the following pseudo code

// This code is in the controller, catches notifications raised by the datasource class
func nsfrcObjectsChanged(notification: NSNotification) {
  self.adapter.performUpdates(animated: true)
}

another possible solution would be to make a custom delegate on the datasource
then make the controller implement the delegate of the datasource

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

and in the controller:

extension MyViewController: MyAdapterDataSourceDelegate {
  func performUpdates(animated: Bool) {
      print("Updating contents of collection view")
      self.adapter.performUpdates(animated: animated)
  }

Hope this clarify a bit and give you a broader vision on the idea of mixing CoreData and ViewModels

Let me know if something is not clear

Hi @racer1988 @rnystrom @jessesquires

It seems to me that we are losing here an important feature of CoreData, that is fetching in batches (fetchBatchSize)?

With what you suggest, for each update (NSManagedObjectContextDidSave) in the Core Data model, we will have to transform all the core data objects returned by the fetch request into IGListDiffable objects?

Whereas with CoreData, out of the box, you can manage a table of 100 000 objects very efficiently. Transforming each core data object into a IGListDiffable object creates a very significant performance penalty, I can't see how it is going to work with 100 000 objects?

Did I miss something?

Umh, @slizeray I think you are right. I guess if you have enough objects to need to use fetchBatchSize then probably you should use directly UICollectionView?

Let's see what IGListKit team says

@slizeray ya if you're using hundreds of thousands of items, IGListKit might not be your best bet today. We do handle thousands of items in Instagram just fine, though. Every technology certainly has its limitations.

Though to fix that I would recommend creating your own queries to load the objects in batches (say 1k at a time or something) and then load the next slice when scrolling near the bottom.

I ended up implementing the viewModel, and now cells disappears.
I am less and less sure on how to bridge core data to IGListKit while keeping the diffing

I would probably recommend not using ListKit with CoreData + FRC in situations like this.

CoreData essentially has it's own optimized stack to handle this kind of thing.

It's not too difficult to catch and batch up the updates for collection view on your own. See:
https://github.com/jessesquires/JSQDataSourcesKit/blob/develop/Source/FetchedResultsDelegate.swift

Closing as resolved via #515

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rnystrom picture rnystrom  路  3Comments

jessesquires picture jessesquires  路  3Comments

kanumuri9593 picture kanumuri9593  路  3Comments

PhilCai1993 picture PhilCai1993  路  3Comments

omerfarukyilmaz picture omerfarukyilmaz  路  3Comments