multiple nsfetchedresultscontrollers or a single one - ios

I am using core data and I am having a UITableView with dynamic number of sections.
I have an entity called dates - and it has let's say a title and a relationship which points to another entity - the id of that entity is the section data will be presented.
Which of the best is the best approach and why?
A. Have an array of NSFetchedResultControllers - in each section I filter the data using a predicate. Then I just present the data to each section.
B. I have a single NSFetchedResultController and I fetch all the data - then inside my CellForRow I check whether I should present them or not.
C. I remove the relationship and I add an extra attribute called sectionId in my entity and use either A or B.
What is best approach in terms of UI performance?
EDIT :
Example - I have
Entity 1 : Data
Data Id : 0, Title : First, (Relationship) section : 0
Data Id : 1, Title : Second, (Relationship) section : 0
Data Id : 2, Title : FisrtB, (Relationship) section : 1
Entiry 2 : SectionsName
SectionId : 0 , Name : TitleA , etc (to-many- relationhsip to Data)
SectionId : 1 , Name : TitleB , etc (to-many- relationhsip to Data)
So, question is actually :
A. Have a FRC that returns all data (3 in total) and then I should
find which is the correct section they go to?
B. Or have a single FRC for each section - first FRC returns the top2
data which I can access via indexPath.row , the same for the second
FRC.

Edited to reflect changes to original question
You almost certainly should use Option B from your original question (Option A in your subsequent edit): a single NSFetchedResultsController, based on the Data entity but with table view sections determined by the section relationship.
Your fetched results controller can do all the hard work of dividing the objects up into the correct sections: ensure that the fetch underlying the FRC is sorted first by section.sectionId (or section.name if you prefer), and specify the FRC's sectionNameKeyPath as section.sectionId (or section.name). The boilerplate FRC/tableView code will then automatically put objects into the correct sections.
The aforementioned boilerplate code:
#pragma mark - TableView Datasource delegate
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.fetchedResultsController.sections.count;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = self.fetchedResultsController.sections[section];
return [sectionInfo numberOfObjects];
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Data *data = [self.fetchedResultsController objectAtIndexPath:indexPath];
....
return cell;
}
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = self.fetchedResultsController.sections[section];
return sectionInfo.name;
}
#pragma mark - Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Data" inManagedObjectContext:self.context];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sectionSort = [[NSSortDescriptor alloc] initWithKey:#"section.sectionId" ascending:YES];
// Add any other sort criteria
....
NSArray *sortDescriptors = #[sectionSort, ...];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.context sectionNameKeyPath:#"section.sectionId" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
aFetchedResultsController.delegate = self;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
#pragma mark - FRC delegate methods
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}

It seems that though you only need one attribute from the entity 2 (sectionID), you still need the relationship in order to get that entity and eventually get that attribute (sectionID). Therefore I suggest option B. The less FRCs the better. You will only need one here. In your CellForRow you will get the current entity in your fetched results array by using the indexPath. Once you have the current entity you can then now access entity 2, which is a property of the first entity. Once you have entity 2, you now have the attribute you've wanted, which is the section id of entity 2. You can now display that attribute (section id) on your table view.

It is almost certainly better to use only one fetched results controller. (I agree with #Lee on this point, but I do not understand why he is recommending option B, which includes more FRCs.)
To summarize your data model:
Section <------->> Date
You can simply fetch the section and just adjust the datasource methods:
// number of sections
return self.fetchedResultsController.fetchedObjects.count;
// number of rows in section
Section *section = [self.fetchedResultsController objectAtIndexPath:
[NSIndexPath indexPathForRow:section inSection:0]];
return section.dates.count;
// cellForRowAtIndexPath
/* create a convenience function in the Section class to return a
sorted array from the NSSet "dates". */
Section *section = [self.fetchedResultsController objectAtIndexPath:
[NSIndexPath indexPathForRow:indexPath.section inSection:0]];
Date *date = section.sortedDates[indexPath.row];
/* configure the cell based on the date object */
So, A is the better option.

Related

NSFetchedResultsController not changing sort order on entity update; does update and not move

My iOS app has a view controller that displays a list of inbox items. These inbox items are an entity in the core data database and the list is managed in the view controller by an NSFetchedResultsController using a main thread managed context (NSMainQueueConcurrencyType).
Except the NSFetchedResultsController, all access to the entities is confined to a code running on a dedicated GCD dispatch queue with a private context. So setting of attribute values in the entity, as well as reading those entity attribute values, happens on this dedicated GCD queue and context.
The view controller with NSFetchedResultsController monitors changes in the main thread context. Changes happen in the background on the dedicated queue and context. Code monitors the NSManagedObjectContextDidSaveNotification notification and when the background thread updates an attribute value, those changes get pushed into the main thread context.
The NSFetchedResultsController is set up to sort on two sort descriptors, a "sortScore" and "date" (only one of which is being updated right now, "date", so every managed object instance has the same "sortScore" do it does not affect the sorting.)
When a "date" change happens, the NSFetchedResultsController notices it and posts the change, but it posts it as an "update" and not a "move", so that the ordering does not change as it should based on the sort descriptors and the "date" being updated in such a way that the new value should fall elsewhere in the ordering.
I am at a loss, after hours of working through and tracing the NSManagedObjectContextDidSaveNotification and every other conceivable point where something could fail, on why the fetched results controller is sending an "update" change and not a "move" change.
This app uses an older forked version of the Robbie Hanson XMPPFramework for managing the Core Data in case that is familiar.
(To compound the issue, once last night it DID work and the moves were happening on one run. I had deleted the app and started from a fresh install and it started to work. But subsequent runs did not work and deleting the app to start fresh did not fix it so that effect of starting fresh was probably a coincidence.)
I have used both logging as well as breakpoints to show that the fetched results controller is sending an "update" instead of a "move".
Here is the code.
Setting up and doing the initial setting up of the NSFetchedResultsController with this create method being called inside the init of the view controller.
#property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
- (NSFetchedResultsController *)createFetchedResultsControllerUsingStorage:(XMPPCoreDataStorage *)storage entityName:(NSString *)entityName
{
NSManagedObjectContext *context = [storage mainThreadManagedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
NSSortDescriptor *sortDescriptorScore = [[NSSortDescriptor alloc] initWithKey:#"sortScore" ascending:NO];
NSSortDescriptor *sortDescriptorDate = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptorScore, sortDescriptorDate, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
[controller setDelegate:self];
[self updateResultsDataSetForController:controller];
return controller;
}
- (BOOL)updateResultsDataSetForController:(NSFetchedResultsController *)controller
{
NSError *error;
BOOL success = [controller performFetch:&error];
if (nil != error) {
NSLog(#"Error fetching inbox items for the inbox view controller. Error = %#", error);
}
return success;
}
The code to handle the changes in the NSFetchedResultsControllerDelegate are basically copied and pasted from the Apple Docs
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.conversationsTable beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.conversationsTable insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.conversationsTable deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.conversationsTable;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
MyAppInboxCell *cell = [tableView cellForRowAtIndexPath:indexPath];
MyAppXMPPInboxBaseMember *inboxItem = [self.fetchedResultsController objectAtIndexPath:indexPath];
MyAppConversation *conversation = [inboxItem conversation];
[cell configureCell:conversation];
}
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}

Core Data and NSFRC showing entries but not persisting through every launch and a Key-Value Coding Error

I am building up a very simple application, allowing users to browse leaflets and videos within the application on a particular topic. One of the features I'm bringing is being able to mark a leaflet or video as a favourite.
The application is UITabBar with 5 tabs and every tab being represented by a UITableViewController. When the user taps to hold on a cell in a tab, it marks it as "starred" and with the use of Core Data and NSFetchedResultsController, the idea is for that entry to appear in the Starred tab.
This is my simple Core Data model:
So when the user taps and holds a cell in any one of the 4 tabs, this is the code I run:
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index
{
switch (index) {
case 0:
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
CustomLeafletVideoTableViewCell *cell = (CustomLeafletVideoTableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];
NSString *cellTitle = cell.customCellLabel.text;
[self moreButtonPressed:cellTitle];
[cell hideUtilityButtonsAnimated:YES];
break;
}
default:
break;
}
}
- (void)cellPressed:(NSString *)passedString
{
NSManagedObjectContext *context = [self managedObjectContext];
Item *item = [NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:context];
Videos *videos = [NSEntityDescription insertNewObjectForEntityForName:#"Videos" inManagedObjectContext:context];
videos.title = passedString;
item.video = videos;
NSLog(#"Passed String = %#", videos.title);
}
I have created a FavouritesTableViewController class and here's the main code:
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)])
{
context = [delegate managedObjectContext];
}
return context;
}
- (NSFetchedResultsController *)fetchedResultsController
{
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Videos" inManagedObjectContext:managedObjectContext];
fetchRequest.entity = entity;
NSPredicate *d = [NSPredicate predicateWithFormat:#"items.video.#count !=0"];
[fetchRequest setPredicate:d];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"title" ascending:NO];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
fetchRequest.fetchBatchSize = 20;
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSError *error;
if (![[self fetchedResultsController] performFetch:&error])
{
//exit(-1);
}
self.favouritesTableView.dataSource = self;
self.favouritesTableView.delegate = self;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.favouritesTableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
#pragma mark Cell Configuration
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
CustomLeafletVideoTableViewCell *customCell = (CustomLeafletVideoTableViewCell *)cell;
Videos *videos = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"What is the video . title %#", videos.title);
customCell.customCellLabel.text = videos.title;
//
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"FavouritesCell";
CustomLeafletVideoTableViewCell *cell = (CustomLeafletVideoTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
#pragma mark NSFetchedResultsControllerDelegate Methods
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
// The boiler plate code for the NSFetchedResultsControllerDelegate
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
default:
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
Issues
My issues seem to stem from the NSFetchedResultsController. In that method, if I leave it as it is with the predicate, when I tap to hold the cell on the other tab, the app crashes with:
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Videos 0x7ffb48d0b910> valueForUndefinedKey:]: the entity Videos is not key value coding-compliant for the key "#count".'
If I remove the predicate line:
// NSPredicate *d = [NSPredicate predicateWithFormat:#"items.video.#count !=0"];
// [fetchRequest setPredicate:d];
when I tap to hold a cell, it marks it as favourite and then when I go to the favourites tab, the entry is there. However, if I launch the app again, the entries in the Favourites tab have gone.
I'm not quite sure what's going on here. Essentially, the Favourites tab is a place for storing the starred items from the user from the other tabs. Do I need a predicate and if I don't, why is the data not persisting through each launch?
The app was set up with Core Data selected, so the AppDelegate has been set up appropriately.
Any guidance on this would be really appreciated.
The video property of Item is a to-one relationship. Basically it's a pointer to an object (or nil). You can't count it, it's not a collection of objects.
So your key path items.video.#count and in particular the video.#count doesn't make sense, hence the crash.
If you want to check if there is a video for a given Item, use #"items.video != nil".
Also you should probably follow conventions and use singular for your objects names (Leaflet and Video) and singular for to-one relationships (item instead of items).

NSFetchedResultsController indexPathForObject not counting sections

I am updating a download view/button on a cell, and when I go to update my cell, I am not getting the correct section.
My code to get the index and update the download progress is this:
Object *obj = (Object *)notification.object;
NSIndexPath *index = [self.fetchedResultsController indexPathForObject:obj];
MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:index];
DownloadProgressButtonView *buttonView = (DownloadProgressButtonView *)cell.accessoryView;
NSNumber *progressLong = [notification.userInfo objectForKey:#"progress"];
float progress = [progressLong floatValue];
NSNumber *totalBytesLong = [notification.userInfo objectForKey:#"totalBytes"];
float totalBytes = [totalBytesLong floatValue];
buttonView.progress = progress *.01;
float totalDownloadEstimate = totalBytes / 1.0e6;
float megaBytesDownloaded = (progress *.01) * totalDownloadEstimate;
cell.bottomLabel.text = [NSString stringWithFormat:#"%.1f MB of %.1f MB", megaBytesDownloaded, totalDownloadEstimate];
If I have two objects, each in a different section, they have the same row (0). When I go to update my cell, it updates the cell in section 1 instead of section 0. How do I fix this?
I can put whatever other code is needed. It works perfectly if I just disable sections in my NSFetchedResultsController.
My NSFetchedResultsController and delegates.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *nameString = [[NSSortDescriptor alloc] initWithKey:self.sectionSortDescriptor ascending:NO];
NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:self.sortDescriptor ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:nameString,descriptor, nil]];
NSString *downloadStartedString = #"Preparing to download";
NSString *downloadingString = #"Downloading";
NSString *downloadPausedString = #"Download paused";
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"(downloaded == YES) OR (downloadStatus like[cd] %#) OR (downloadStatus like[cd] %#) OR (downloadStatus like[cd]%#)",downloadPausedString, downloadStartedString,downloadingString];
[fetchRequest setFetchBatchSize:20];
_fetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext sectionNameKeyPath:self.sectionNameString
cacheName:nil];
_fetchedResultsController.delegate = self;
self.fetchedResultsController = _fetchedResultsController;
return _fetchedResultsController;
}
/*
NSFetchedResultsController delegate methods to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:(StudioTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
NSLog(#"A table item was moved");
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"A table item was updated");
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
Finally when the download status changes, I update the object and send a notification to update the cell with the new status:
- (void)updateCell:(NSNotification *)notification
{
Object *obj = (Object *)notification.object;
NSIndexPath *index = [self.fetchedResultsController indexPathForObject:obj];
[self.tableView reloadRowsAtIndexPaths:#[index] withRowAnimation:UITableViewRowAnimationFade];
}
Updating cells this way is not reliable. A cell updated in such a way will sooner or later be reused. The cell's subviews will be re-configured by tableView:cellForRowAtIndexPath:, based on the data provided by the datasource.
You should make changes to the Object itself (instead of passing them in notification's userInfo) and save the managed object context. Then NSFetchedResultsControllerDelegate callbacks will fire, allowing you to reload the corresponding row. Then you should set all the properties of MyTableViewCell in configureCell:atIndexPath.
And the configureCell: method should be called from cellForRowAtIndexPath method, not from the fetched results controller delegate method. The general pattern is to call reloadRowsAtIndexPaths: in controllerDidChangeObject:. Otherwise you can run into some cell reuse issues.
An idea on how the code should look like:
- (void)updateCell:(NSNotification *)notification
{
//depending on your Core Data contexts setup,
// you may need embed the code below in performBlock: on object's context,
// I omitted it for clarity
Object *obj = (Object *)notification.object;
//save changes to the object, for example:
NSNumber *progressLong = [notification.userInfo objectForKey:#"progress"];
obj.progress = progressLong;
//set all the properties you will need in configureCell:, then save context
[obj.magagedObjectContext save:&someError];
}
then the fetched results controller will call controllerDidChangeObject:, in this method you should reload the row:
case NSFetchedResultsChangeUpdate:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
finally, configure the cell (let's assume that you call configureCell:atIndexPath from tableView:cellForRowAtIndexPath:):
- (void)configureCell:(MyTableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath {
Object *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
DownloadProgressButtonView *buttonView = (DownloadProgressButtonView*) cell.accessoryView;
buttonView.progress = object.progress.floatValue *.01;
//and so on
}

Cannot update CoreData Model after getting a Fetch Request

My app is set up like this, I have two view controllers coming off of my RootViewController - a simple main selection page with two buttons. The top button sends me to ViewController 1 where I take a photo and insert data in 6 text fields about the photo. I then hit a save button which saves those entities to my ManagedObjectModel TargetData inside my ManagedObjectContext. The second button on the main page leads to a TableVIewController where I have called an NSFetchedResults sorta thing to update the TableView.
It works pretty well...at least until I run the TableViewController once. I can add as many photos with text data as I want until I show the TableViewController, at which point, upon leaving the page, the TableViewController will only show the objects it loaded the first time I opened the page, not any items added after that. I have done a little searching and have found that after loading the TableVIew for the first time, the fetchResults is only seeing however many entities it saw the first time it loaded. For example, if I started the app, added 5 photos with text, then ran the TableViewController, I would see 5 items correctly displayed. I could go back to the main page and then add another photo with text, but if I went back to the TableVIewController I would only see the 5 photos I added before opening the TableViewController.
In the class for the TableViewController I have set the and am using all of it's normal methods inside my TableViewController.m file.
Totally clueless. Help!
EDIT: Here is some of my code
From: ViewController.m that will save data
- (IBAction)saveTarget:(id)sender {
//Only save if there is an image other than the stock photo
if (self.image != [UIImage imageNamed:#"Photo-Video-Slr-camera-icon"]) {
//Grab the main ManagedObjectContext from the AppDelegate
sTCAppDelegate *appDelegate =[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context =[appDelegate managedObjectContext];
//Get the Target model from CoreData
[self setTarget:[NSEntityDescription insertNewObjectForEntityForName:#"TargetData" inManagedObjectContext:context]];
//Set the properties of the TargetData model
self.target.weaponData = self.weaponData.text;
self.target.bulletType = self.bulletType.text;
self.target.stanceType = self.stanceType.text;
self.target.distanceData = self.distanceData.text;
self.target.targetNotes = self.targetNotes.text;
self.target.sightType = self.sightType.text;
self.target.scoreData = [NSNumber numberWithInt:[self.scoreData.text intValue]];
//Set image to smaller size for storage
UIImage *image = [self resizeImage:self.image toWidth:50 andHeight:50];
//Save as PNG NSData for compression
self.target.targetImage = UIImagePNGRepresentation(image);
//Set a date property to use for organizing by most recently saved
self.target.timeStamp = [NSDate date];
//Save to context
NSError *error = nil;
if ( ![context save:&error] ){
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
//Jump back to main page
[self.navigationController popToRootViewControllerAnimated:YES];
//set images back to nil for next time AddTarget is opened
image = nil;
self.image = nil;
NSLog(#"Data Saved");
}
//Return alert if user has not entered a photo
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"We're Sorry!" message:#"You must enter at least a photo to save target data." delegate:nil cancelButtonTitle:#"Okay" otherButtonTitles:nil];
[alert show];
}
}
From: TableViewController.m that shows data
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
NSLog(#"number of sections:%lu",(unsigned long)[[self.fetchedResultsController sections] count]);
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"%lu", [sectionInfo numberOfObjects]);
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSLog(#"Ran Cell Configure");
[self configureCell:cell atIndexPath:indexPath];
// Configure the cell...
return cell;
}
-(void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath{
TargetData *target = [self.fetchedResultsController objectAtIndexPath:indexPath];
UIImage *image = [UIImage imageWithData:target.targetImage];
cell.imageView.image = image;
cell.textLabel.text = [NSString stringWithFormat:#"%#", target.scoreData];
cell.detailTextLabel.text = target.weaponData;
}
#pragma mark- FetchedResultsControllerDelegate Methods
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
sTCAppDelegate *appDelegate =[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context =[appDelegate managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"TargetData" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:0];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedRequestsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedRequestsController.delegate = self;
self.fetchedResultsController = aFetchedRequestsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (IBAction)mainMenu:(id)sender {
[self.navigationController popToRootViewControllerAnimated:YES];
}
So yes, I am using the Methods in the TableViewController implementation.
EDIT 2
I usually run my app on my actual iPhone to test it because then I can use the camera in my device. To see what the console might say about my problem, I added some images to the simulator and ran it on the simulator. This time, after adding an photo with text, opening the TableViewController, then adding another photo, I got a huge crash error report after trying to open the TableViewController again.
Here is the terminating part of the error:
2014-01-13 13:09:38.759 Target Tracker[25124:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'CoreData: FATAL ERROR: The persistent cache of section information does not match the current configuration. You have illegally mutated the NSFetchedResultsController's fetch request, its predicate, or its sort descriptor without either disabling caching or using +deleteCacheWithName:'
Any clue what that means?
RESOLVED
The problem was that I was caching the FetchResults when I ran the TableViewController. When I added another entity to the model and tried to return a new fetchResult, it didn't match the cached version which returned a critical CoreData error. I didn't see it as an error because I was not originally running the app in a simulator, but instead on my actual device. Once I ran it in the simulator I was able to see this error.
In short- I needed to set my "cacheName" to nil when I initialized the NSFetchedResultsController
NSFetchedResultsController *aFetchedRequestsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
For more information see this post: NSFetchedResultsController crashing on performFetch: when using a cache
The problem was that I was caching the FetchResults when I ran the TableViewController. When I added another entity to the model and tried to return a new fetchResult, it didn't match the cached version which returned a critical CoreData error. I didn't see it as an error because I was not originally running the app in a simulator, but instead on my actual device. Once I ran it in the simulator I was able to see this error.
In short- I needed to set my "cacheName" to nil when I initialized the NSFetchedResultsController
NSFetchedResultsController *aFetchedRequestsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
For more information see this post: NSFetchedResultsController crashing on performFetch: when using a cache
are you sure that you are saving the context after adding objects to it?
After inserting objects, call
- (BOOL)save:(NSError **)error;
The NSFetchedResultsController will not update until it receive a save context notification. This is usually performed when you call the method:
[yourManagedObjectContext save:&error]
when the fetched results controller receive the update, some calls are performed to its delegate, see the docs for something called NSFetchedResultsControllerDelegate and implement all of it's methods to update your tableview. That was the right way, you can also implement only one method to see if this works:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
// In the simplest, most efficient, case, reload the table view.
[self.tableView reloadData];
}
It sounds like you are not implementing the NSFetchedResultsController delegate methods. Perhaps show some of the code for your UITableViewController?
Update
Ok, your code looks good; really good.
So the obvious, easy possibilities are out. Next step is to put some break points (or logs) into the -saveTarget:, -fetchedResultsController and the delegate methods and make sure everything is firing.
With the code you provided you should be seeing updates. Which hints at something not being fired.

NSFetchedResultsControllerDelegate & UITableViewDelegate Behavior

I have encountered behavior with the FRC and TableView delegates that appears to be inconsistent:
The initial call to performFetch causes the delegate methods to be called as expected. However, if I update the predicate on the FRC's baseFetch and then call performFetch again, the FRC delegate methods are never called, and the TableView is not updated with new data. To force the table to update and display the new data I am explicitly calling reloadData.
The initial call to performFetch correctly builds the TableView's sections. If I change FRCs (the new FCR has a different fetchRequest and sectionNameKeyPath) and call performFetch again, the table and sections update to match the new results, as expected. HOWEVER, if I then go back to the original FRC and call performFetch, the sections have the correct names but with the previous FRC's section/row structure. To force the sections to update I am explicitly calling reloadSectionIndexTitles.
My feeling is that the delegates should be firing anytime we call performFetch if something changes. Explicitly calling reloadData and reloadSectionIndexTitles seems like an expensive and unnecessary step since that's what the delegates exist for. Am I missing something?
Here's the relevant code:
NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.myTable beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.myTable insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.myTable deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[self.myTable cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.myTable deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[self.myTable reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.myTable insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.myTable deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.myTable endUpdates];
}
UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return [[currentFCR sections] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[currentFCR sections] objectAtIndex:section];
return section.name;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
id <NSFetchedResultsSectionInfo> sectionInfo = [[currentFCR sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
Fetching
- (NSFetchedResultsController *)FRC1 {
if (FRC1 != nil) {
return FRC1;
}
NSFetchRequest *request = [[DataProvider instance] getFetch];
[[DataProvider instance] sortListByFirstSort:request];
[request setFetchBatchSize:20];
FRC1 = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:[[DataProvider instance] managedObjectContext]
sectionNameKeyPath:#"groupName"
cacheName:nil];
return FRC1;
}
- (NSFetchedResultsController *)FRC2 {
if (FRC2 != nil) {
return FRC2;
}
NSFetchRequest *request = [[DataProvider instance] getFetch];
[[DataProvider instance] sortListBySecondSort:request];
[request setFetchBatchSize:20];
FRC2 = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:[[DataProvider instance] managedObjectContext]
sectionNameKeyPath:#"name"
cacheName:nil];
return FRC2;
}
-(void)doFetch{
NSError *error;
if (![currentFCR performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
[self.myTable reloadData];
[self.myTable reloadSectionIndexTitles];
}
NSFetchedResultsControllerDelegate methods controllerWillChangeContent:, etc, are only called if a delegate is set on an NSFetchedResultsController instance. I don't see any delegates set in your code.
The initial table load (or explicitly calling reloadTable) will cause the UITableViewDataSource methods to be called, fetching data from the currentFCR. The NSFetchedResultsControllerDelegate methods will not be called if a delegate is not set on an NSFetchedResultsController.
Also, NSFetchedResultsControllerDelegate methods are only called if any of the relevant managed objects change (or are added/deleted) within the NSManagedObjectContext.
If you want to replace the NSFetchedResultsController object you will need to reload the table explicitly.

Resources