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.
Related
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;
}
}
I have a chat panel in an iOS application using NSFetchResultController. Fetching occurs as:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
DataSource *dataSource = [DataSource getInstance];
[NSFetchedResultsController deleteCacheWithName:#"MessageList"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:_CHAT_MESSAGE_ENTITY_ inManagedObjectContext:dataSource.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
//Predicate
DataClass *dataClass = [DataClass getInstance];
NSPredicate *predicate;
if ([self.chatType intValue] == SINGULAR_CHAT_TYPE) {
Contact *receiver = [self.members objectAtIndex:0];
predicate = [NSPredicate predicateWithFormat:#"(senderUid LIKE %# AND receiverUid LIKE %#) OR (senderUid LIKE %# AND receiverUid LIKE %#)", [dataClass getKeychainValueWithKey:_USER_ID_IDENTIFIER_], receiver.contactUid, receiver.contactUid, [dataClass getKeychainValueWithKey:_USER_ID_IDENTIFIER_]];
}
else { // group chat
}
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"messageDate" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:dataSource.managedObjectContext sectionNameKeyPath:#"messageId" cacheName:#"MessageList"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
// 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 in fetchedResultsController performFetch %#List, %#", _CHAT_MESSAGE_ENTITY_,error, [error userInfo]);
//abort();
}
return _fetchedResultsController;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.chatTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[self.chatTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.chatTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
default:
return;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.chatTableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
// case NSFetchedResultsChangeUpdate:
// [self configureCell:[chatTableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
// break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.chatTableView reloadData];
[self.chatTableView endUpdates];
}
This fetching works normal, when I open the view controller all previous messages are seen on the table as they should be. However, when I type a new message and press "send" button, not only the new message but also last message before it is not getting fetched.
I get the number of rows, see there are 2 missing objects.
Still, if I stop app and run again, these last 2 messages are getting fetched and shown.
The same problem is occuring conversations panel. In other words, after sending message my conversation view controller needs to be updated. However, its last conversations (chosen one) is getting lost.
I'm saving the message and conversations as:
DataSource *ds = [DataSource getInstance];
ChatMessage *newMsg = (ChatMessage*)[ds createInsertObject:_CHAT_MESSAGE_ENTITY_];
[newMsg loadFromDictionary:tmpDict];
Chat *chat = (Chat*)[ds findOrCreateWithIdentifier:[[headers objectForKey:CHAT_TYPE] stringValue]:[headers objectForKey:#"receiverUid"] :_CHAT_ENTITY_];
[chat loadHeaders:headers andMessage:newMsg];
newMsg.chat = chat;
[ds coreDataSaveContext];
And after this saving, I'm telling to my chat view controller:
- (void)refreshPage {
[self.fetchedResultsController performFetch:nil];
[self.chatTableView reloadData];
[self takeTableContentsUp];
}
Am I missing something about fetch result controller? Why is this problem happening and how can I fix it?
Thank you!
EDIT: If I remove "Chat" entity in saving message, just save the message everyting is ok for chat panel. Thus the problem is multiple entities interacted with each other.
How should i edit and save 2 entities to core data, and fetch without problem?
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).
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.
Using core data on a on an application that has tabbed views. The second tab loads the core data, no errors show up in Xcode or when I run the app but when I click on the second tab the app crashes with the error "An Instance of NSFetchedResultsController requires a non-nil fetch request and managedObjectContext.
I'm new to core data and really struggling with this error so would appreciate any help I can get. The implementation file has the following code
- (void)setupFetchedResultsController
{
// 1 - Decide what Entity you want
NSString *entityName = #"EatCategory"; // Put your entity name here
NSLog(#"Setting up a Fetched Results Controller for the Entity named %#", entityName);
// 2 - Request that Entity
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
// 3 - Filter it if you want
//request.predicate = [NSPredicate predicateWithFormat:#"EatCategory.name = Blah"];
// 4 - Sort it if you want
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name"
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
// 5 - Fetch it
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
[self performFetch];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setupFetchedResultsController];
if ([[self.fetchedResultsController fetchedObjects] count] == 0) {
NSLog(#"No Results were fetched so nothing will be given to the table view");
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Eat Category Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// configure the cell...
EatCategory *eatcategory = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = eatcategory.name;
return cell;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates]; // Avoid NSInternalInconsistencyException
// Delete the role object that was swiped
EatCategory *eatCategoryToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting (%#)", eatCategoryToDelete.name);
[self.managedObjectContext deleteObject:eatCategoryToDelete];
[self.managedObjectContext save:nil];
// Delete the (now empty) row on the table
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self performFetch];
[self.tableView endUpdates];
}
}
Are you sure you are setting the managed object context properly? It seems that the fetch request is ok but what about the context? If the context is not set properly this could lead to that error.
Do you inject the context from an external object? If yes, how is declared the managedObjectContext property?
For example:
// from an external object
YourController *controller = ... // alloc-init the controller
controller.managedObjectContext = self.managedObjectContext;
// within your controller .h
#property (strong, nonatomic) NSManagedObject* managedObjectContext; // or retain if you don't use ARC
// within your controller .m
#synthesize managedObjectContext;
You could also grab the main context inside your controller from the application delegate (if you have declared it there) like the following:
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext* managedObjectContext = delegate.managedObjectContext;
but this could lead to a more rigid application design.
Some notes
Instead of using commitEditingStyle you could "register" for NSFetchedResultsControllerDelegate callbacks. This class has been created to deal with changes in table views. You could use it and respond in different manner for table changes.
Here the class reference for NSFetchedResultsControllerDelegate.
Hope it helps.