NSFetchedResultsController not firing controllerDidChangeContent - ios

My current setup:
I intend to use a UISegmentedControl to switch between the data i'd like to show in a tableview.
I have been using restkit for all restful interactions to the webservice. The data is stored in a coredata sqlite store. I have setup a NSFetchedResultsController in the following fashion.
-(void)setupFetchedResultsControllerWithFetchRequest:(NSFetchRequest *)fetchRequest{
self.fetchedResultsController=nil;
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:#"address" ascending:YES];
fetchRequest.sortDescriptors = #[descriptor];
NSError *error = nil;
// Setup fetched results
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[RKManagedObjectStore defaultStore].mainQueueManagedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
[self.fetchedResultsController setDelegate:self];
BOOL fetchSuccessful = [self.fetchedResultsController performFetch:&error];
// NSAssert([[self.fetchedResultsController fetchedObjects] count], #"Seeding didn't work...");
if (! fetchSuccessful) {
ShowAlertWithError(error);
}
}
and this is how i have written fetch requests to pass into the fetchedresultscontroller.
-(NSFetchRequest *)prepareFetchRequestForGlobalAtm{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"AtmBranches"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"branchType='0'"];
[fetchRequest setPredicate:predicate];
return fetchRequest;
}
I have mated them together in the segmentedControlToggledMethod appropriately.
[self setupFetchedResultsControllerWithFetchRequest:[self prepareFetchRequestForGlobalAtm]];
//Restkit Call
[self loadGlobalAtms];
this is my delegate method for NSFetchedResultsController.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.locationsTableView reloadData];
}
My Logic here is that every toggle should fire methods to setup the NSFetchedResultsController according to the values that have been passed. I can see clearly in the logs that i have values coming in from the webservice and are being cached, but the tableview never populates itself.

You havn't change the data, so you can't receive the notification.
When you performFetch with self.fetchedResultsController, you have already got all the objects.
You can receive the delegate's callback when you insert/update/delete in the mainManagedObjectContext or its child ManagedObjectContext
You should do like this
[self.fetchedResultsController setDelegate:self];
BOOL fetchSuccessful = [self.fetchedResultsController performFetch:&error];
// NSAssert([[self.fetchedResultsController fetchedObjects] count], #"Seeding didn't work...");
if (! fetchSuccessful) {
ShowAlertWithError(error);
}
[self.locationsTableView reloadData];
And keep the logic when delegate callback

Related

Core Data to-many relationship fetching

I have the following set up in core data: A Dish entity, with many ingredients, and a Ingredient entity with one dish.
I'm having trouble fetching my to-many relationship data, i.e. the ingredients for a certain dish. I'm trying to populate my collection view cells with the ingredients associated to each dish.
I have my core data model set up as follows:
I have tried various methods to fetch the data.
One of the things I've tried is to do the following: Dish *dish = [self.fetchedResultsController objectAtIndexPath:indexPath]; and then NSLog dish.ingredients (which returns a NSSet), but the returns the whole core data item, i.e., the createdAt date, the name and everything, together with the unformatted version of the ingredients for that dish.
Another thing i tried was to create a new instance of the Ingredient entity with Ingredient *ingredient = [dish.ingredients valueForKeyPath:#"ingredientName"];, but that returns an unformatted string (I want it UTF8 encoded before it's displayed because it contains special characters).
A lot of solutions when googling suggests using NSPredicate, but I just can't figure out how to use that. I'm still new to core data, and XCODE in general, so any help here would be appreciated, i.e. where do you implement the NSPredicate, and what would the correct predicate be in my case?
A bit of supporting code:
my collectionViewController:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSManagedObjectContext* context = [RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext;
self.managedObjectContext = context;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Dish" 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:#"createdAt" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[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.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
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 %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
cellForItemAtIndexPath:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FOFPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"photo" forIndexPath:indexPath];
self.object = [self.fetchedResultsController objectAtIndexPath:indexPath];
Dish *dish = [self.fetchedResultsController objectAtIndexPath:indexPath];
Ingredient *ingredient = ???;
cell.backgroundColor = [UIColor lightGrayColor];
[cell.imageView setImageWithURL:[NSURL URLWithString:[[NSString stringWithFormat:#"http://192.168.0.3:4000%#", dish.dishImage] description]] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
}];
cell.titleLabel.text = dish.dishName;
cell.headerLabel.text = dish.dishHeader;
cell.ingredientsLabel.text = [NSString stringWithFormat:#"%#", nil];
NSLog(#"%#", ingredient);
[self.collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
return cell;
}
The simple way is your first approach and then only use the attributes that you care about from the ingredients. Core Data is more about programming with objects than it is about the underlying database, so attempting to simulate DB queries isn't usually the right approach.
I also recommend getting rid of all the valueForKey stuff and using actual object properties so that the code is a bit clearer and the compiler can help you out more with data types.
If you're looking for Ingredients only for a one specific Dish, you have to set your fetchRequest for Ingredients (and not Dish) and set the predicate on Dish. To make the following code work, you may need first to Create NSManagedObject Subclasses (with the help of Xcode Editor button).
#import "Dish.h"
#import "Ingredient.h"
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Ingredient"];
[fetchRequest setFetchBatchSize:20];
//if dishname are unique values, set your predicate on it.
//You can also set your predicate on the dishID if dishName are not unique values (several dishes with a same name)
NSString *theDishIWant = #"Ratatouille"; //whatever Dish name you want
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"dish.dishName == %#", theDishIWant];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"ingredientname" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
NSLog (#"%#", _fetchedResultsController);
return _fetchedResultsController;
}
Your configureCell: atIndexPath: method may look like this:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
Ingredient *ingredient = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = ingredient.ingredientName;
cell.detailTextLabel.text = ingredient.ingredientValue;
}

Updating an NSFetchedResultsController with an NSPredicate that uses the current time

I have an application that needs to filter objects based on timestamps. For example, lets say I want to filter an Event to only display Events that are in the past. I want to then display them in a UITableView. I would set up an NSFetchedResultsController like so:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" 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:#"timeStamp" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
// Filter based on only time stamps in the past
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timeStamp < %#", [NSDate date]];
fetchRequest.predicate = predicate;
[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.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
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 %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
My question is this: what is the best way to update this view so that the filter is based on the current time? My existing solution is to set up a method like this:
- (void)updateFetchedResultsController {
self.fetchedResultsController = nil;
[self.tableView reloadData];
}
Then I call that method on viewWillAppear: or viewDidAppear:. This works unless the user stays on the screen for a while.
I could also use an NSTimer and call updateFetchedResultsController once a minute or so but that causes issues if the user is scrolling through the table. Is there a better way to check if the data has changed? Since the data isn't changing I can't rely on any save events.
You only ever need to change the data on display when an items time is no longer valid. It has a date so you can calculate how long into the future that is and set a timer. You order the data so the next item to expire is always the first in the list.
To finesse, you can check for scrolling when the timer expires and delay the reload until the scroll animations have completed.

Core Data: How to pass a managed object to a UITableViewCell and do a fetchRequest from there?

I have a UITableView With a custom Cell that contains another UItableView. I need to pass an object to it, and then execute a fetchrequest based on this object.
From my cellForRowAtIndexPath method, I am passing a Managed Object to the custom Cell, like this:
TWHistoryViewStandardExpendedCell *cell = (TWHistoryViewStandardExpendedCell *)[tableView dequeueReusableCellWithIdentifier:ExpandedCell];
if (cell == nil) {
cell = (TWHistoryViewStandardExpendedCell *)[[[NSBundle mainBundle] loadNibNamed:#"HistoryViewStandardCellExpanded" owner:self options:nil] objectAtIndex:0];
}
Day *aDay = (Day *)[_fetchedResultsController objectAtIndexPath:indexPath];
[cell setViewingDay: aDay]; // NSLog here returns the expected object! :)
return cell;
This way, I should be able to execute a fetchRequest based on this object, from my custom Cell.
On my custom UItableViewCell, I do this:
- (void) awakeFromNib
{
[self fetchRequest];
}
- (void)fetchRequest {
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
Some details about my fetchedResultsController: see in comments that my managed object returns null here!
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController) {
return _fetchedResultsController;
}
NSManagedObjectContext * managedObjectContext =
[myAppDelegate managedObjectContext];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Clock"
inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
NSLog(#"_viewingDay: %#", _viewingDay); // returns null! :(
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks); // also returns null!
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"clockIn" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
//
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
_fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Based on this, I have overriden my setViewingDay method:
- (void)setViewingDay:(Day *)viewingDay
{
if (_viewingDay != viewingDay) {
_viewingDay = viewingDay;
NSLog(#"setViewingDay: %#", _viewingDay); // returns expected object! =)
[self fetchRequest];
}
}
Still, after this, my UITableView remains empty. My numberOfRowsInSection method keeps returning 0!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
NSLog(#"numberOfRowsInSection %d", [sectionInfo numberOfObjects]);
return [sectionInfo numberOfObjects];
}
I also added a UIButton that performs the fetch request. Hoping to find a situation where the fetchRequest was being performed before the actual setting of my reference to the managed object. So I click on it, do perform a fetch. And nothing! No rows whatsoever.
EDIT:
However, the NSLogs bellow, from inside my fetchResultsController do return an object then, by using a supporting UIBUtton and IBAction. But still no rows!
NSLog(#"_viewingDay: %#", _viewingDay);
NSLog(#"_viewingDay.clocks: %#", _viewingDay.clocks);
Supporting UIButton to perform a fetch:
- (IBAction)fetchem:(UIButton *)sender {
[_clocksTableView reloadData];
_fetchedResultsController = nil;
[NSFetchedResultsController deleteCacheWithName:nil];
[self fetchRequest];
NSLog(#"\n\nfetchem: %# \n\nvd: %# \n\nclocks: %# ", _fetchedResultsController.description, _viewingDay.description, _viewingDay.clocks.description );
}
Anything I might be missing here?
Thank you!
EDIT 2:
I just realized that setting my predicate to (1 == 1), returns all clocks. Confirming that my resultsController is correctly set up. There may be something wrong with my predicate... I don't see what. I have a similar predicate in a previous controller and works great.
Predicate is very simple, nothing fancy:
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:#"day == %#", _viewingDay]];
I have a Clock and Day entities.
A Day has many clocks. 1 to many relationship.
A clock has one day, and one day only.
The fetchRequest above should have been returning all clocks from that day. But it's not?
1) awakeFromNib gets execute the moment you instantiate the cell and therefore before you set the viewing day. It also doesn't get execute if the cell is reused, so you should trigger the fetch request separately.
2) How does the fetchedResultsController property inside the cell get set?
EDIT 1:
From the datamodel and the predicate, it would seem the relationship between clock and day doesn't get set correctly.
EDIT 2:
Quoting the asker's comment from below: "The problem was fixed after inserting: [self fetchRequest] inside my overriden setter."

NSFetchedResultsController not informed about model changes

Got somehow lost on something what worked in other projects and now stopped working.
I've got a controller (A) (subclass of NSObject) which has a NSFetchedResultsController for entities of class kClassCycleQuestionDetails.
My ManagedObjectContext is initiated with NSMainQueueConcurrencyType.
In another controller (B) I start inserting entities of the above class and start a save on every tenth object.
[self.moc performBlock:^{
[self prepareNewCycle];
}];
The context posts the NSManagedObjectContextDidSaveNotification (received in B) but I never see the controllers delegate method - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller called in A.
Both controller share the same managed object context, so I should see model changes?!
Has anyone an idea.
Edited:
For clarification: prepareNewCycle inserts a cycle entity into the context and than
adds entities of kClassCycleQuestionDetails to the cycle entity (to-many relationship).
Here's the code for the controller A's fetchedResultsController
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:kClassCycleQuestionDetails inManagedObjectContext:self.moc];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
[fetchRequest setPredicate:[self predicateForExamCycleMode:self.cycle.examQuestionModeValue cycleId:self.cycle.cycle_id]];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:kQCQDCycleQuestionDetails_id ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[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.moc
sectionNameKeyPath:nil
cacheName:NSStringFromClass([self class])];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[NSFetchedResultsController deleteCacheWithName:NSStringFromClass([self class])];
NSError *error = nil;
if (![_fetchedResultsController performFetch:&error]) {
ALog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}

Recipes for changing the Fetch Request for NSFetchedResultsController and reloading table data

From apple doc Modifying the Fetch Request I see that is possible to change the NSFetchRequest for NSFetchedResultsController. Steps are easy to set up.
After invoking performFetch: I think there is the need to call reloadData on the the table view. How to perform such call?
Reading some stackoverflow topics, I've seen that calling that method should work in most cases. But is there a right way to do it?
In How to switch UITableView's NSFetchedResultsController (or its predicate) programmatically?, TechZen wrote that:
Just make sure to send the tableview itself a beginUpdates before you
swap controllers and then an endUpdates when you are done. This
prevents the table from asking for data in the narrow window when the
FRC are being swapped out. Then call reloadData.
Could you explain exactly what does it mean?
Assuming that the logic to generate the correct fetch (some kind of conditional statement) is in the getter of your NSFetchedResultsController instance. Then it is really easy
self.fetchedResultsController = nil; // this destroys the old one
[self.tableview reloadData];
// when the table view is reloaded the fetchedResultsController will be lazily recreated
Edit: adding complete code sample of something I've done. Basically I have a NSDictionary entityDescription that hold values to customize the creation of a NSFetchedResultsController. If I want to change the fetchRequest I change my entityDescription variable to indicate the new values and override the setter to reset the fetchedResultsController and reload the table. It gives you the basic idea.
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil) {
return __fetchedResultsController;
}
if (self.entityDescription == nil) {
return nil;
}
// Set up the fetched results controller.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[self.entityDescription objectForKey:kEntityName]];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
if ([[self.entityDescription objectForKey:kEntitySortField] isEqualToString:#"null"] == NO) {
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[self.entityDescription objectForKey:kEntitySortField] ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[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.moc sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return __fetchedResultsController;
}
- (void)setEntityDescription:(NSDictionary *)entityDescription
{
_entityDescription = entityDescription;
self.fetchedResultsController = nil;
[self.tableView reloadData];
}

Resources