Unexpected inserts sent to NSFetchedResultsController - ios

I have a model with Managers and Employees. Managers have a relationship to employees. I have a UITableViewController utilizing FRC. The FRC is initialized over the Manager model with a sort descriptor on the name.
The normal things work - adding managers and deleting managers work as expected. However, what's strange is that when an employee of an existing manager is updated (changing some coredata property on employee), FRC sends an insert on the manager of the employee being updated. CoreData obviously gets confused, and reports an assertion:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section
after the update (8) must be equal to the number of rows contained in
that section before the update (8), plus or minus the number of rows
inserted or deleted from that section (2 inserted, 1 deleted) and plus
or minus the number of rows moved into or out of that section (0 moved
in, 0 moved out). with userInfo (null)
The strange thing is that in the assertion, it seems to think that 2 inserts were performed with 1 delete. I logged the didChangeObject and I only see a single insert.
Why is an insert being sent for the Manager when it's employee is updated?
Update - showing the code
Here's my view controller's initialization of FRC. Note that I'm modeling companies, so managers belong to companies (which explains the predicate:
let fetchRequest = NSFetchRequest(entityName: "ManagerModel")
fetchRequest.predicate = NSPredicate(format: "%K == %#", "company", self.currentCompany)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
Here's my view controller's didChangeObject function:
func controller(controller: NSFetchedResultsController,
didChangeObject anObject: NSManagedObject,
atIndexPath indexPath: NSIndexPath?,
forChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath?)
{
let manager = anObject as! ManagerModel
switch(type)
{
case NSFetchedResultsChangeType.Insert:
log.info("Inserting row at index path \(newIndexPath!), \(manager.name)")
break
case NSFetchedResultsChangeType.Delete:
log.info("Deleting row at index path \(indexPath!), \(manager.name)")
break
case NSFetchedResultsChangeType.Update:
log.info("Updating row at index path \(indexPath!), \(manager.name)")
break
case NSFetchedResultsChangeType.Move:
log.info("Moving row at index path \(indexPath!) to \(newIndexPath!), \(manager.name)")
break
}
}
My code syncs data and updates data. Here is the code that updates the employee by first searching for it.
let cdEmp = cdMgr.findEmpoyeeById(mailId: downloadedEmp.empId)
if (cdEmp != nil)
{
// If I don't update the cdEmp model, I have no problems.
// If I update any of the cdEmp properties, I'm getting
// the insert notification on the manager instance cdMgr
cdEmp!.name = downloadedEmp.name
}

Related

How does NSFetchedResultsController know what selected range of row to read from CoreData instead of reading entire all rows?

My understanding on NSFetchedResultsController is that, it will not load entire all rows from CoreData at once.
Based on the current visible UI table row on screen, NSFetchedResultsController will just load a small amount of rows, just to be good enough to be shown on screen.
But, when I try to read and test the official code from Apple, this is contrast with my perception.
https://github.com/yccheok/earthquakes-WWDC20/blob/master/LoadingAndDisplayingALargeDataFeed/Earthquakes_iOS/QuakesViewController.swift#L123
There are around 10,000++ items in the CoreData database.
When the app is started for the 1st time, UITableView is not being scrolled yet, and the following function being executed for the 1st time
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count = dataProvider.fetchedResultsController.fetchedObjects?.count ?? 0
print(">>> SIZE \(count)")
// Try to inspect the content...
var quake = dataProvider.fetchedResultsController.fetchedObjects?[count-1]
print("----> \(quake?.place)")
return count
}
When setting up NSFetchedResultController, I also try to change the batch size explicitly to 1, just to see what is the outcome - https://github.com/yccheok/earthquakes-WWDC20/blob/master/LoadingAndDisplayingALargeDataFeed/Shared/QuakesProvider.swift#L284
/**
A fetched results controller to fetch Quake records sorted by time.
*/
lazy var fetchedResultsController: NSFetchedResultsController<Quake> = {
// Create a fetch request for the Quake entity sorted by time.
let fetchRequest = NSFetchRequest<Quake>(entityName: "Quake")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
fetchRequest.propertiesToFetch = ["magnitude", "place", "time"]
// Just for testing purpose to avoid NSFetchedResultsController
// load all data at once into memory.
fetchRequest.fetchBatchSize = 1
// Create a fetched results controller and set its fetch request, context, and delegate.
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: persistentContainer.viewContext,
sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = fetchedResultsControllerDelegate
// Perform the fetch.
do {
try controller.performFetch()
} catch {
fatalError("Unresolved error \(error)")
}
return controller
}()
When app launched for first time, the following outcome is printed
>>> SIZE 10902
----> Optional("43 km E of Teller, Alaska")
My questions are
Currently, there are only less than 20 visible UI table row shown on screen. But, it seems like CoreData has already load all 10,000++ rows at once? Is this an expected behaviour for NSFetchedResultsController. As, my understanding is having NSFetchedResultsController, is to avoid having to load all unnecessary data into memory in 1 time.
I do not see a way, on how UITableView communicate back to NSFetchedResultsController regarding the visible UI row range. Isn't UITableView suppose to tell NSFetchedResultsController that "Currently, UI row 0th till 14th are visible on screen. Please only load data in range 0th till 14th from CoreData database"?
Thanks.
You are mistaken in your understanding of NSFetchedResultsController and its purpose. It makes it easier to use Core Data with table views, particularly where objects are inserted, deleted or updated; You can use delegate methods to efficiently handle these actions without needing to reload the entire table view.
That said, there is a default behaviour of Core Data that means, in effect, only visible rows are loaded into memory regardless of whether you use an NSFetchedResultsController or not.
The default behaviour of Core Data is to only provide fault objects at first; A fault is a lightweight representation of an object. When a property of the object is accessed Core Data fetches the full object from the persistent store and loads it into memory.
This may have the effect of only loading full objects for on-screen rows since cellForRow(at:) is only called for visible rows (plus a few extra to allow for scrolling). If the only access to your fetched objects is in that function, then the faults will only fire for those rows.
As more rows become visible, cells will be required and more faults will fire, loading more data.
You can disable this faulting behaviour but it will increase memory use.
You can use UItableViewDataSourcePrefetching to fire faults ahead of display, but in many cases you won't see a difference in the UI.

fetchBatchSize and cache in NSFetchedResultsController ignored

I am using an NSFetchedResultsController to display messages in a chat room app.
The context variable is assigned in appDelegate and a reference to that context used in the chat room.
let context = persistentContainer.viewContext
I initialize the NSFRC as follows in viewDidLoad:
func initializeResultsController() {
let request = NSFetchRequest<Message>(entityName: "Message")
let messageSort = NSSortDescriptor(key: "dateCreated", ascending: true)
request.sortDescriptors = [messageSort]
request.predicate = NSPredicate(format: "chatRoomId == %#", self.chatRoomId)
request.fetchBatchSize = 30
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "messageDateSectionIdentifier", cacheName: self.chatRoomId)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}
The sectionNameKeyPath ("messageDateSectionIdentifier") is a derived property so that the sections can be divided into calendar days.
I have two problems. Firstly the batchSize seems to be ignored and secondly the cache seems to make no difference to the performance. The more messages the longer the delay when selecting the chat room. about 1 second for 1500 messages.
When I edit scheme to show coreData info in console, the batch request for 30 rows is performed multiple times when the view first appears and in one case the array size is 1500. Not sure whether that is the fault array or the populated array. The console printOut is:
CoreData: annotation: sql connection fetch time: 0.0013s
CoreData: annotation: total fetch execution time: 0.0014s for 1454 rows.
CoreData: annotation: Bound intarray _Z_intarray0
CoreData: annotation: Bound intarray values.
And this is repeated after this multiple times with value of 30 rows.
I have tried simplifying the sectionNameKeyPath to just dateCreated to see if the derived sections were the problem but there was no difference at all. I should also mention that as with all chat apps, the app initially scrolls to the bottom when it is presented.
What I want is for the cache to work and also for the fetchBatchSize to work so that only 30 rows are fetched from coreData initially until the user starts to scroll up. The delay now caused by this method is having a measurable impact on my app performance.
You are correct that batchSize is not respected by a fetchedResultsController. A NSFetchedResultsController does a fetch and then tracks all changes in the context to see if anything is added, removed, moved or changed. If it only fetched a subset of the matching entities by respecting batchSize it would be unable to do it's job.
You can get around this by setting a predicate to only fetch message after a certain date. In order to figure out what is the cutoff date you can do a single fetch first, where batchSize = 1 and the batchOffset = [how many message you want initially in you fetchedResultsController]. As more message come in the collection will increase in size beyond your initial limit.
Also be aware that the sectionNameKeyPath is called for EVERY element in the collection. So doing even a small amount of work there can cause huge delays. Don't create a calendar or a dataFormatter in then sectionNameKeyPath - reuse a single one.
I finally found the cause of the problem in my case.
If you are referencing the fetchedResultsController in tableView heightForRowAt then the fetchBatchSize will loop through and load all data in loops of the fetchBatchSize you specify.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let item = self.fetchedResultsController!.object(at: indexPath)
// get and return height of item
return item.heightOfItem
}
If you use UITableViewAutomaticDimension or define a height that doesn't require a reference to fetchedResultsController (i.e. a fixed height) then you won't have this problem and fetchBatchSize will work as it should.
Unfortunately I found UITableViewAutomaticDimension not acceptable for scrolling performance so I guess I have to manually configure batch loads using offsets.
I also had one additional issue that was causing the loop loads of data. That was the sectionNameKeyPath was in my case a transient property. This also caused problems but unfortunately is necessary.
If you are having problems with fetchBatchSize with an NSFetchedResultsController I would advise looking at these two issues.

Adding new object to CoreData with one to many relation

I have CoreData model like this:
Parcel can have only one company, but company can have multiple parcels to deliver.
I have three companies in database preloaded. I have created table view with sections and loading data via NSFetchedResultsController.
I'm configuring it like this:
let fetchRequest = NSFetchRequest(entityName: EnityNames.PackageInfoEnityName)
// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: PackageInfoKeyPaths.Company, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: PackageInfoKeyPaths.Company, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
When I'm launching app on simulator I have three sections(as expected), i want to have sections as companies displayed, so I'm using relationship as section name key path:
number of sections 3
FedEx
UPS
DHL
I have created popover where I can add new entries to database to populate list with additional data. This is the code I'm using to create and save new data:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let package =
NSEntityDescription.insertNewObjectForEntityForName(EnityNames.PackageInfoEnityName, inManagedObjectContext: managedObjectContext) as! PackageInfo
let formatter = NSNumberFormatter()
formatter.numberStyle = .DecimalStyle
package.parcelNumber = formatter.numberFromString(parcelNumberTextField.text!) ?? 0;
package.createdAt = NSDate()
let company = avaialbleCompanies![companyPickerView.selectedRowInComponent(0)]
package.company_relation = company
company.addPackageToCompany(package)
appDelegate.saveContext()
the companies array is passed in prepare for segue to my little popover to let user choose only companies that are inside database. After this I'm getting something strange:
number of sections 4
FedEx
UPS
UPS
Why it is adding new Section? It should just add new item to existing section!
and terrifying error:
PackageChecker[5336:281349] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of sections. The number of sections contained in the table view after the update (4) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
Important part can be that I'm getting this:
updated index path 1x0
and I'm using fetchedResultsController.sections?.count to get number of sections.
If I restart the app on sim my newly added record is present in proper section inside list. Why it is not updating properly on runtime?
P.S. I have:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
Edit:
If I remove sectionNameKeyPath: PackageInfoKeyPaths.Company - adding works perfectly. Can You help me with this sections? Maybe I'm configuring them poorly.
The crash is occurring because the FRC is creating a new section, but your code does not currently create a corresponding tableView section. If you implement the NSFetchedResultsControllerDelegate method:
controller:didChangeSection:atIndex:forChangeType:
that should fix the crash.
But I'm afraid I can't see why the FRC is creating a new section.

Swift editing related Core Data objects with NSFetchedResultsController across views / how do I format and use NSPredicate with a block?

I have a question regarding editing/deleting Core Data objects in a UIListView that are related to another object.
To simplify the problem, let's say I have a list view of Family objects. Tapping on a Family name opens a list view of FamilyMember objects for the chosen Family object. I have a one to many relationship between Family and FamilyMember set up in Core Data.
I perform an NSFetchRequest to grab the list of all 'Family' objects in my initial table view controller and put them into an NSFetchedResultsController. When a family in the list is tapped, that 'Family' object is passed into the FamilyMembersViewController with prepareForSegue.
The issue I'm facing is that since I passed in a 'Family' object directly from the initial view controller to FamilyMembersViewController, I never had to create a FetchRequest or a NSFetchedResultsController in FamilyMembersViewController. Therefore, some editing functions that worked great on the initial view now don't work in the FamilyMembersViewController.
How do I take advantage of NSFetchedResultsController functions (such as the following) in a view that doesn't have a NSFetchedResultsController? Do I need to re-query Core Data, limiting the results to a specific 'Family" object? It seems like a waste since I already have the object available in my view.
Am I approaching this functionality incorrectly? I guess I'm just hoping to have the functions be similar from one view controller to the next.
I've tried making a new array called 'familyMembers' and a corresponding NSFetchedResultsController within my FamilyMembersViewController and predicated it with this:
var predicate = NSPredicate(format: "family == %#", "\(self.family)")
where self.family is the 'Family' object passed from the initialView. But in this case, for some reason the "self.family" part just returns an empty string, when really it should return the Family object. Even if it did return the Family object, it doesn't seem like passing that into the predicate would help anything, since the predicate seems to match only strings. I can't quite figure out how to format a Predicate with a block statement.
Any help would be much appreciated! Thank you for reading.
func controllerWillChangeContent(controller: NSFetchedResultsController!) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
default:
tableView.reloadData()
}
families = controller.fetchedObjects as [Family]
}
func controllerDidChangeContent(controller: NSFetchedResultsController!) {
tableView.endUpdates()
}
You are on the right track, though predicate arguments don't have to be strings - you can pass the family object itself:
var predicate = NSPredicate(format: "family == %#", self.family)

How to implement NSFetchedResultsController delegate when multiple row is on deletion?

Two items is deleted.
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
switch(type) {
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
default:
break
}
}
At deletion line I get this error:
CoreData: error: Serious application error. Exception was caught
during Core Data change processing. This is usually a bug within an
observer of NSManagedObjectContextObjectsDidChangeNotification.
Invalid update: invalid number of rows in section 0. The number of
rows contained in an existing section after the update (0) must be
equal to the number of rows contained in that section before the
update (2), plus or minus the number of rows inserted or deleted from
that section (0 inserted, 1 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out). with
userInfo (null)
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of rows in section 0. The number of rows contained in an
existing section after the update (0) must be equal to the number of
rows contained in that section before the update (2), plus or minus
the number of rows inserted or deleted from that section (0 inserted,
1 deleted) and plus or minus the number of rows moved into or out of
that section (0 moved in, 0 moved out).'
You need to 'batch' the updates by also implementing the controllerWillChangeContent and controllerDidChangeContent delegate methods as follows (although sorry these are Obj-c):
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}

Resources