README and documentationIGListKit version:It is a question and request.
In this question
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()
}
}
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
Your model is made of NSManagedObject
class User: NSManagedObject {
var name: String?
var birthday: Date?
}
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:
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)
}
}
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
var fetchResultController: NSFetchedResultsController<Stuff>
fetchResultController.delegate = self
extension DataSource: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.adapter.performUpdates(animated: true)
}
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