Reload UITableView data every time context changes? - ios

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.

Related

How to check inserted and deleted items in UICollectionView, seeing unexpected deleted item

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.

NSFetchedResultsController (and UITableView) delegate methods calls growing and growing

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

Table not updated after addition

I want my app to check the core data store at start-up. If the store is empty, it will add two items into it. What is the best way to implement this?
Can I put the following code in viewDidLoad?
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MonitorItem"];
self.monitorItemArray = [[managedObjectContext
executeFetchRequest:fetchRequest
error:nil] mutableCopy];
// If the core data is empty, populate it with the two compulsory items
if ([self.monitorItemArray count] == 0)
{
self.AddMandatoryItems;
}
[self.tableView reloadData];
}
I have searched other articles but none seems to give me an answer that I could understand.
Getting information from managedObjectContext in viewDidLoad is good.
If in cellForRowAtIndexPath you populate the cells from self.monitorItemArray than there is no reason to call reloadData (Which essentially erase the entire table view and re-draw it from scratch - which is exactly what happens when the view is appearing on screen any way...).
If you also show information from a web service, you can call reloadData in the response method to replace the existing data with the one that came from the web. Otherwise - if only information from core data is shown on the table view - no need for reloadData (Or maybe only in a case where the information in your managedObjectContext has changed while the table view is on screen).

iOS: Saving managed object context takes over 1 second after a while

Strange problem, and strangely only since yesterday. I have a fetched results controller in which you can reorder the rows without any problem. After a fresh start of the app, everything works fine and fast. However if you change from this tab bar to another tab bar and edit some random textfield (which isn't even linked to core data and doesn't trigger any save), the reordering is very slow. And I was able to narrow it down only to the save context. But now I have no clue where to look further. Any advice? Here's my reorder function:
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
if (self.activeField && [self.activeField isFirstResponder]){
[self.activeField resignFirstResponder];
}
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
//Makes only a mutable copy of the array, but NOT the objects (references) within
NSMutableArray *fetchedResults = [[self.fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving
NSManagedObject *resultToMove = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[fetchedResults removeObject:resultToMove];
// Now re-insert it at the destination.
[fetchedResults insertObject:resultToMove atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 1;
for (MainCategory *fetchedResult in fetchedResults)
{
fetchedResult.position = [NSNumber numberWithInt:i++];
}
// Save
TICK;
[((AppDelegate *)[[UIApplication sharedApplication] delegate]) saveContext];
TOCK;
self.suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}
Yesterday I've only inserted these two methods (which cannot be the problem, because after commenting them so they don't trigger, the same problem remained):
-(void) registerForKeyboardNotificationsWithTabBarHeight:(CGFloat)tabbarHeight andHeaderHeight:(CGFloat)headerHeight andFooterHeight:(CGFloat)footerHeight andBottomSpacingToTextFields:(CGFloat)spacingToTextFields
{
self.tabbarHeight = tabbarHeight;
self.footerHeight = footerHeight;
self.headerHeight = headerHeight;
self.spacingToTextFields = spacingToTextFields;
// Register notification when the keyboard will be show
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
// Register notification when the keyboard will be hide
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
FetchedResultsController:
- (void)setupFetchedResultsController
{
self.managedObjectContext = ((AppDelegate *)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"MainCategory"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"position" ascending:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil cacheName:#"MainCategoryCache"];
}
And the view will appear is also very harmless..
- (void)viewWillAppear:(BOOL)animated
{
//Initialize database
[super viewWillAppear:animated];
[self displayBudget];
/*
This is only necessary since the sum of all categories is calculated and the FetchedResultsController
doesn't get this since it's only monitoring a MainCategory and the changes occured in the Subcategories */
[self.tableView reloadData];
}
I'm thankful for any clues what could be the problem :-(
I would not recommend multi-threading in this situation. The issue is that your saves are being done at the wrong time. Moving them to a background queue is not going to solve the issue. You need to change your save behavior.
Writing to disk is expensive. There is no shortcut around it. Even if you move your saves into another context (there is an Apple example on this) they are still doing to take time and potentially block your UI (you cannot do a new fetch during a save). You need to change your save behavior.
You do not need to save every time a user moves a row. That is wasteful. You want to queue up your saves so that you are doing more each save. In addition, you should be saving when the user expects there to be a delay. When you push or pop a view might be a good time for a save. When your app goes into the background is a great time for a save.
Saving when the user wants smooth UI response is bad and causes a bad user experience. Look for opportunities where the user is expecting something to take time. Posting something to a server? Save at the same time.
Your NSFetchedResultsController, etc. will read unsaved data from your main NSManagedObjectContext just as it will saved data. You are not gaining anything on the UI by saving frequently.
Update 1
Step one in performance problems is to run your application in Instruments.
Measure the speed and then see what is costing you time. Time Profiler is your friend.
Yes it will slow drawing your UI because you should't use saving and creating objects in main Queue;
read this https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
you should have another background thread and you should do inserting deleting stuff there and then you have to pass NSObjectID_s for newManagedObject_s and then get it in main thread:
NSManagedObjectContext * mainThreadContext;
NSManagedObject * object = [mainThreadContext objectWithId:objectID];

refreshing an nsfetchedresultscontroller not tied to a uitableview

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]);
}

Resources