I have two UIViewControllers in my app (relevant to this question).
Each of them has an NSFetchedResultsController with a request on the same entity called News. They both have the same sort descriptors too and they use the same UITableViewCell subclass to display everything. They also both conform to NSFetchedResultsControllerDelegate and run the delegate methods.
The only difference is that the first view controller, I will call it SummaryVC, only displays the first (up to) 6 objects fetched. Whereas the other, NewsFeedVC shows all the objects and it also pages the download of more objects. Because of this the delegate methods in SummaryVC just runs [self.tableView reloadData];
When first launching the app the SummaryVC is displayed and triggers a download of the first 6 News objects (converted from JSON) and saves them in a BG thread.
The FRC then picks up the save and displays the entities.
However...
Intermittently (I hate that word) after pushing and popping between different parts of the app. I come back to SummaryVC and the app will crash.
It is always the same crash too.
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[_PFArray objectAtIndex:]: index (46) beyond bounds (6) with userInfo (null)
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[_PFArray objectAtIndex:]: index (46) beyond bounds (6)'
In this case there were already more than 6 entities loaded in to Core Data. The 6 makes me suspicious of the FRC that belongs to SummaryVC.
I have tried several things to fix this.
Set the FRC delegate to nil on viewWillDisappear.
Set the FRC to nil on viewWillDisappear.
When the delegate methods call check that the VC is actually the VC on the screen.
In NewsFeedVC viewWillDisappear it now cancels all the download operations from its queue.
The problem seems to occur as a result of the saveContext being picked up by the FRC. i.e. if I go into the NewsFeedVC and trigger a download but then pop the VC before it finishes then that seems to trigger the crash.
Now, code wise. The crash is never with my own code.
I can show the FRC setup...
- (NSFetchedResultsController *)fetchedResultsController
{
if (!_fetchedResultsController) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"News"];
NSSortDescriptor *dateSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"date" ascending:NO];
NSSortDescriptor *sourceSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"source" ascending:YES];
NSSortDescriptor *titleSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES];
fetchRequest.sortDescriptors = #[dateSortDescriptor, sourceSortDescriptor, titleSortDescriptor];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.moc sectionNameKeyPath:nil cacheName:#"AllNewsItems"];
_fetchedResultsController.delegate = self;
NSError *error = nil;
[_fetchedResultsController performFetch:&error];
if (error) {
NSLog(#"Error performing fetch: %#, %#", error, [error userInfo]);
}
}
return _fetchedResultsController;
}
I'm thinking maybe I should pass the FRC from one object to the other. I.e. inject it into a property so that there is only one. Is that a valid thing to do?
EDIT
Could it be to do with using the same cache name for goths fetched results controllers possibly?
EDIT2
Nope, it still crashes if I change the cache name.
EDIT3
OK, I can replicate this every time. It happens if I start the NewsFeedVC scrolling and then while it is scrolling I pop back to the SummaryVC.
Thanks to Martin R for the fix.
I removed the cacheName from the initialisation of the FetchedResultsController and it fixed the problem.
Thanks.
Related
I am struggling to integrate UICollectionView and NSFetchedResultsControllerDelegate in this particular case.
In my view controller, I fetch a Parent entity, but display its multiple Child entities in the collection view. When creating a new Parent entity in this view controller (with insertNewObjectForEntityForName) and automatically creating a Child entity and adding it to the parent entity with addChildObject, I can add more Child entities by pressing a button, and save the object successfully. Unfortunately, for some reason the NSFetchedResultsControllerDelegate methods are not called, specifically, controllerDidChangeContent is never called and the collection view doesn't get updated.
When I fetch an already existing Parent entity, and then try to change it by adding new Child objects, the app crashes with the following exception:
*** Assertion failure in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:],
/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3505.17/UICollectionView.m:4211
And gives out this error in the end:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
controllerDidChangeContent:. Invalid update: invalid number of items
in section 0. The number of items contained in an existing section
after the update (5) must be equal to the number of items contained in
that section before the update (4), plus or minus the number of items
inserted or deleted from that section (1 inserted, 1 deleted) and plus
or minus the number of items moved into or out of that section (0 moved
in, 0 moved out). with userInfo (null)
What puzzles me the most, is that it shows that there is a deleted item in (1 inserted, 1 deleted), when, in fact, I am only adding an item (initializing a Child entity with insertNewObjectForEntityForName and adding it with [parent addChildObject:child].
In all these cases, I am not saving the context. I expect adding objects to the parent entity to trigger NSFetchedResultsControllerDelegate methods.
FetchResultsController setup code:
- (NSFetchedResultsController *) fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [self emojiFetchRequest];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Initializing parent object in viewWillAppear:
[[self fetchedResultsController] performFetch:&error];
id fetchedObject = [[[self fetchedResultsController] fetchedObjects] firstObject];
if (fetchedObject != nil) {
self.parent = (Parent *)fetchedObject;
self.navigationItem.title = self.parent.name;
} else {
self.navigationItem.title = #"New parent";
self.parent = [NSEntityDescription insertNewObjectForEntityForName:#"Parent" inManagedObjectContext:[self managedObjectContext]];
Child *child = [NSEntityDescription insertNewObjectForEntityForName:#"Child" inManagedObjectContext:[self managedObjectContext]];
[self.parent addFramesObject:frame];
}
Adding new child objects:
Child *child = [NSEntityDescription insertNewObjectForEntityForName:#"Child" inManagedObjectContext:[self managedObjectContext]];
[self.parent addChildrenObject:child];
[self.collectionView reloadData]; // if this isn't done, the crash happens
// but this can't animate changes
Core Data Model:
Is there a way I can see those objects? I've tried logging didChangeObject, and I only see one insert. I am not deleting anything. If anyone has any other ideas, I'd be glad to hear them.
EDIT:
If I call [self.collectionView reloadData] after adding the objects, everything seems to work correctly without crashing. But, it would be nice for the changes to be animated, which cannot be done with reloadData, and I sense there is something fundamentally wrong with what I'm doing and I would like to fix it.
Best solution: Fetch the Child entity in your fetched results controller, filter by parent.
For inserts, use the plain vanilla implementation of the NSFetchedResultsControllerDelegate. This will provide a nice animation or let you add your own.
Setup: UITableView with FRC. Rows are simple list of text content user can pull to refresh to get the latest.
I’m seeing strange behavior where cellForRow is called for each row, multiple times. So I see it for 0,0 0,1 0,2 0,3 (visible rows), but these 4 rows all have cellForRow called multiple times. But the first time you view the list they're called once. The second time, twice, etc. By the 7th time, after the user sees the content, behind the scenes it continues to try and configure the cell over and over and eventually crashes.
So if you go to any list of content, it hits the server, downloads the stories, creates NSMOs and displays. In the logs, I see configureCell called once for each visible row. If I refresh, I see the same. BUT if i navigate to a different screen, then come back, when I pull to refresh I notice that cellforrow is called twice for each row. If I continue this process of leaving and coming back, every time I do, cellforrow is called an additional time. Logging some of the fetched results controller delegate methods, I see willchangecontent before each set of cellforrow calls. Can someone help me determine why my cellforrow method is called a growing number of times?
One idea was the way I was setting up FRC. I followed code like CoreDataBooks and moved things to viewdidload, but still seeing issue.
I have a property in the .h and in the .m have what i thought was a standard setup:
- (NSFetchedResultsController *)fetchedResultsController
{
//NSLog(#"fetchedresulscontroller");
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
// initialize fetch request. setup predicate, sort, etc.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"date" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
// perform actual fetch. delegate methods take it from here
NSError *fetchError = nil;
if (![self.fetchedResultsController performFetch:&fetchError])
{
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", fetchError, [fetchError userInfo]);
abort();
}
return _fetchedResultsController;
}
andrewbuilder was on the right track. It all had to do with the FRC, but the trick was the third party SWReveal library used for the menu. Turns out, I was creating a new VC each time (previous wasn't deallocated) and the FRC was looking at all live view controllers. So each time i tapped a selection from the menu, another was added and the config calls were called for that.
The solution is to nil out the FRC delegate in viewwilldisappear and set it in viewwillappear
i am wrting an app that uses a number of NSFetchedResultsControllers (FRCs), each for a different Managed Object subclass in my data model. several of these FRCs are part of UITableViewControllers, but several are not. i have two questions: one, why is one of my FRCs not updating when i add a new object to the MOC, and two, should i be using an FRC at all for this purpose? i thought it would save me from having to put fetch requests in all over the code, but perhaps it only makes sense to use an FRC with a tableview or other such object.
so, here are the details.
one of the FRCs not tied to a tableview is one that keeps track of schedules. the SetScheduleViewController has the following properties (among others), which are passed in via the parent view controller:
#property (weak, nonatomic) NSFetchedResultsController *scheduleFetchedResultsController;
#property (weak, nonatomic) NSManagedObjectContext *managedObjectContext;
this FRC was created by another object (which maintains a strong pointer to it) via the following code
- (NSFetchedResultsController *)scheduleFetchedResultsController {
if (_scheduleFetchedResultsController != nil) {
return _scheduleFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Schedule" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"start" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *fRC = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
NSError *error = nil;
if (![fRC performFetch:&error]) {
IFLErrorHandler *errorHandler;
[errorHandler reportErrorToUser:error];
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
self.scheduleFetchedResultsController = fRC;
return self.scheduleFetchedResultsController;
}
(an aside: the reason the cacheName is set to nil is that i seem to be able to assign a cache name to only one of the many FRCs in this app, or the app crashes...if only one FRC is given a cache name and the rest have cache names set to nil, all seems well, though i am concerned that without a cache the performance may be terrible as the size of the persistent store grows...but that's another matter)
within the SetScheduleViewController, a schedule can be created or deleted, and on loading the SetScheduleViewController the most recently created schedule is loaded. when a schedule is created a new Schedule Managed Object is created, and then the MOC is saved as follows:
Schedule *newSchedule= [NSEntityDescription insertNewObjectForEntityForName:#"Schedule" inManagedObjectContext:self.managedObjectContext];
newSchedule.start=newStartTime;
//etc
NSError *saveError;
if (![self.managedObjectContext save:&saveError]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", saveError, [saveError userInfo]);
abort();
this successfully saves the new MO, but the FRC does not update...i've checked this several ways...for example, if after saving a new schedule i re-enter the SetScheduleViewController and check [[self.scheduleFetchedResultsController fetchedObjects] count] it is not incremented. however, if i quit the app and open it again, lo and behold, the FRC fetched object count is incremented and the new object is indeed there.
i should note that the scheduleFetchedResultsController does not have a delegate assigned, unlike the FRC's attached to tableviewcontrollers, which are all working fine. i didn't assign a delegate because from what i could tell, the FRC delegate methods in the tableviewcontrollers only deal with updating the tableview when the FRC changes content...i do not see any delegate methods that refresh the FRC itself when a new object is added and the MOC saved.
so, my two questions again are: 1) why is the FRC not refreshing (and how can i make it refresh), 2) does it even make sense to use an FRC to manage fetched results for a managed object not tied to a tableview, and should i instead simply perform a fresh fetch from the MOC every time i need access to the list of objects?
any help much appreciated.
In the NSFetchedResultsController documentation it is stated that the FRC is in "no-tracking" mode if no delegate has been set. Also, the delegate must implement at least one of the change tracking delegate methods in order for change tracking to be enabled.
The delegate does not have to be a table view controller, so you could use your
SetScheduleViewController as a delegate and just implement the controllerDidChangeContent: delegate method. Inside that method, the updated
fetchedObjects is available, and you can e.g. update any UI elements in the view
controller.
Update: Passing the FRC from the parentVC does not make much sense. Each view controller should have its own FRC. So scheduleFetchedResultsController should be a method in the childVC. And as the FRC is created "lazily" in the getter method, the getter has to be called somewhere.
In the case of table view controllers, this happens because all table view data source methods
access self.fetchedResultsController.
If your childVC does not access self.fetchedResultsController then the FRC
will not be created. That could be the reason why calling [self.fetchedResultsController performFetch:&error] in viewDidLoad, as suggested in the other answer, solved your problem.
The delegate method controllerDidChangeContent: will then be called if the result
set changes during the lifetime of the childVC. That's where using an FRC makes sense.
If you just want to fetch the current set of objects when the childVC is loaded then
you don't need a FRC, and you can just execute a simple fetch, e.g. in viewDidLoad.
I've faced the same problem before. The reason is you didn't performed the fetch of FRC.
Add the following code on -viewDidLoad:
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
I have a UITableViewController that's a subclass of CoreDataTableViewController (it's the Stanford class). That implements a fetchedResultsController.
Now, in my viewWillAppear, I have this:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.managedObjectContext) {
[self useManagedDocument];
}
}
It initializes the managedObjectContext if I don't have one, and gets it from a helper class. In the MOC setter, I initialize the fetchedResultsController:
- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
_managedObjectContext = managedObjectContext;
if(self.managedObjectContext) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:CD_ITEM_ENTITY_NAME];
request.sortDescriptors = #[[NSSortDescriptor
sortDescriptorWithKey:CD_ITEM_NAME_PROPERTY
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
request.predicate = nil;
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
} else {
self.fetchedResultsController = nil;
}
}
When my program starts, it loads the table data up correctly and my debugger says there was a fetch request made. However, after inserting data into my Core Data graph, and saving, it says the context changes and fires this delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
/*NSError *error;
[controller performFetch:&error];
if(error) {
NSLog(#"%#", error);
}
[self.tableView reloadData];*/
}
I commented this because it wasn't working. Basically, what I want to do is reload the data every time the context changes. If I add an item in another view controller and then go back to this one, it should reload in that case too.
How do I implement this? I tried doing performFetch in that delegate method and it entered it (I checked by setting a breakpoint inside), but the performFetch did nothing and my table wasn't reloaded.
When I add an item in a modal VC (another one I have for managing items) this is what happens in my logger:
2013-05-10 22:41:38.264 App1[7742:c07] [ItemCDTVC performFetch] fetching all Item (i.e., no predicate)
2013-05-10 22:41:46.454 App1[7742:c07] NSManagedObjects did change.
2013-05-10 22:41:46.456 App1[7742:c07] NSManagedContext did save.
When I close my app but do not quit it from the multitasking bar, and then reopen it, it does nothing. No fetch. Well, if the context didn't change I don't want it to fire a request, but imagine if the user adds an item in another ViewController and then goes back to my ItemCDTVC, which lists all items. Does it get a context changed notification so it can call the delegate method to update the table, or will I always have to refresh regardless of changes in my viewWillAppear? I currently have it set to do it only once, on app load.
Fixed, all I had to do is put a one liner in that delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
This ends updates, inserts, deletes, and changes made to the table (refreshing my view, essentially) the same as per Apple's documentation.
It now updates on content change.
I spend the whole night debugging a simple app. The app retrieves one image (yes one..intend to make my life easier) from web, and displays it in table view. I do that as a practice to learn Core Data. Before I fix it, the error message shows below:
2012-09-30 06:16:12.854 Thumbnail[34862:707] 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 (1) must be equal to the number of sections contained in
the table view before the update (0), plus or minus the number of
sections inserted or deleted (0 inserted, 0 deleted). with userInfo
(null)
Basically it's saying that something goes wrong with FRC delegate methods. At one hand, section number changes from 0 to 1. At the other hand, "0 inserted, 0 deleted". So how the section number can increase? That should not happen.. Hence the error.
I fix the bug by simply adding [self.tableView reloadData] to my FRC setup method. I got the inspiration from this post, yet I don't quite understand it. The answer seems like too complicated and project specific. Could someone explain why adding reloadData can fix the bug? The answer might be an easy one, I hope so.
Key components of my app, if that matters:
Use UIManagedDocument to establish the core data stack
Create a helper method to download images from Flickr API
In NSManagedObject subclass file, try fetch image from persistent store. If it's not there yet, insert it into MOC.
- (void)setupFetchedResultsController
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"BigImage" inManagedObjectContext:self.document.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *imageDescriptor = [[NSSortDescriptor alloc] initWithKey:#"image" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject: imageDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
// Create fetch results controller
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.document.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController.delegate = self;
NSError *error;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Error in performFetch: %#, %#", error, [error userInfo]);
}
// Critical!! I add this line to fix the bug!
[self.tableView reloadData];
}
A fetched results controller tracks only changes to the managed object contents after the first fetch. These changes are then propagated to the table view using the delegate methods didChangeSection, didChangeObject etc.
But there is no automatic mechanism that the result of the initial fetch is sent to the table view. That is the reason why you have to call reloadData after performFetch.
However, there is a situation where this seems to work automatically. UITableViewController calls reloadData in viewWillAppear (if the table is loaded the first time). Therefore, if you set up the FRC for example in viewDidLoad, reloadData will be called in viewWillAppear, and you don't have to call it manually.