Using AFIncrementalStore, I'm trying to fetch an entity's data set when a controller loads. I have something similar working on a previous controller, and attempted to follow that pattern:
- (void)viewDidLoad
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ItemCategory"];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedStandardCompare:)]];
fetchRequest.returnsObjectsAsFaults = NO;
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[appDelegate managedObjectContext] sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
[_fetchedResultsController performFetch:nil];
}
Then later, I try to grab that data to fill a UIPickerView in a table cell:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSArray *rows = [_fetchedResultsController fetchedObjects];
NSLog(#"%#",rows);
...}
But fetchedObjects returns an empty array. The only way I can get data is if I do a refetch just before I call "fetchedObjects" like so:
_fetchedResultsController.fetchRequest.resultType = NSManagedObjectResultType;
[_fetchedResultsController performFetch:nil];
But then I'm hitting the server a second unnecessary time. As far as I can tell, both fetches are executed in the exact same way. So why does it take a second fetch before the data is filled?
My theory is that the second fetch is actually somehow making the first fetch's data available, not actually storing new data, but my understanding of CoreData is a little too shaky for me to determine exactly what is happening. Thanks for helping me sort through this!
edit I meant to mention that I tried the solution to this similar question to no avail: https://stackoverflow.com/a/11334792/380643
Did you remember to do a reloadData on your table?
I discovered I left out the critical delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView reloadData];
}
After implementing this, the data came in after the first fetch.
Related
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
I populate my tableview from core data. The rows are sorted according to a primary and a secondary keys. The viewDidLoad method contains the MOC and fetch request code.
Users can update the row in the tableview through a different view and the data is promptly updated as display. However the data displayed is now not in proper sorting order.
My problem is how do I get the data sorted and displayed accordingly? Should I invoke the viewDidLoad method again to re-fetch the data and then reload the tableview? What is the best way to do this? If I should invoke the viewDidLoad method, where should this be done?
My code is shown below:
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MonitorItem"];
[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:
[[NSSortDescriptor alloc] initWithKey:#"show"
ascending:NO],
[[NSSortDescriptor alloc] initWithKey:#"expiry"
ascending:YES], nil]];
self.monitorItemArray = [[managedObjectContext
executeFetchRequest:fetchRequest
error:nil] mutableCopy];
}
If you don't use FetchedResultsController then you mosts likely store the objects in some collection. You can then add observer (KVO - KeyValueObserving) to this collection to be notified when something changes in its objects. Then you can handle this response and invoke [tableView reloadData];
What you should not do is to invoke viewDidLoad: on your own. If you still think about it you should rather put your view initialization code into separate method which is called in the viewDidLoad: and you can then also use it when you need it.
Have a UITableViewController to display scores from a game.
The scores will display 1 ,5, 1, 13
The order they were saved in, but after looking at the scores two or three times the scores they just start changing order. The scores will display
5, 1, 1, 13 or 1, 13, 1, 5
I save the scores using this code
-(void)saveDate
{
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *newScore = [NSEntityDescription insertNewObjectForEntityForName:#"Scores" inManagedObjectContext:context];
NSNumber *theScore = [[NSNumber alloc]initWithInt:self.score];
[newScore setValue:theScore forKeyPath:#"score"];
}
My entity name is "Scores" and I have have one attribute in it called "scores".
I load the Core Data in the TableView inside ViewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Scores"];
self.scores = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
[self.tableView reloadData];
}
self.scores is a #property NSMutableArray that I copy the data into so I use in the NumberOfRows I use [self.scores count];
This is the code for my managed object context.
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication] delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedObjectContext];
}
return context;
}
To display the cells I use this code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *number = [self.scores objectAtIndex:indexPath.row];
[cell.textLabel setText:[NSString stringWithFormat:#"Score: %#", [number valueForKey:#"score"]]];
}
I use this exact code for another app and the data in the table always displays in the exact order it was saved. I have read a lot of the documentation and searched other places before using the method for an answer. I do understand why the information is changing order. Thanks for your help!
You need to set the sortDescriptors of your NSFetchRequest. I can't answer why the same code in another app is returning the data in the same order every time. Nothing I've seen in the docs says anything about the order of returned results during a fetch request that doesn't have a sort descriptor attached. For example if you had an entity with a property called 'name' you would want something like
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
Few notes about your code.
First to retrieve an ordered result you should use a sort descriptor against your score attribute.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"score"
ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
Second, do NOT run a request without passing nil for the error and checking the returned value.
NSError *error;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle the error.
}
Finally, take advantage of NSFetchedResultsController when dealing with UITableViews.
I've got a more or less basic Master Detail app. I've followed this tutorial to get familiar with Core Data, and now I'm trying reorder my cells on my Master TVC. Everything is working fine, including the successful reordering of my cells. However, when I dig down and view one of the detail VCs, I return to the original, alphabetized ordering. I believe it has something to do with the NSSortDescriptor "sortDescriptor" that was included in the tutorial. I am not sure how to remove it, or how to give it different characteristics. Any help is appreciated. Below is my NSFetchedResultsController method.
-(NSFetchedResultsController*) fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Top" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"topName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
fetchRequest.sortDescriptors = sortDescriptors;
_fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
EDIT:
After much research over the past few days, I'm realizing it's more of an issue with my moveRowAtIndexPath method. Anyone have any suggestions or recommendations of working with Core Data and the ability to reorder cells? Also, does this require custom tableviewcell class?
Your table view is sorted based on a name. Given that the names are fixed, if you reorder cells 'by hand' and then return to the table view, the sorting based on the name is reestablished.
To get what you want you'll need to add a field to your Core Data model called something like sortIndex. You then sort the table with a sortDescriptor based on sortIndex; you can initialize sortIndex to the creation order. When the User reorders cells through the UI, you'll also change the sortIndex for all the impacted cells/managed-objects. Then, since sortIndex is part of the Core Data model, when the User ends and restarts your App, their preferred sorting will be reestablished.
Sometimes, for modeling reasons, you don't want to include a sortIndex directly in some managed object. For example, a Person has first and last names but not a sortIndex (in fact). You can create an association, perhaps a DisplayPreferences managed object, with a one-to-one mapping between a Person and a DisplayPreferences and having a sortIndex in DisplayPreferences.
A few things you need to check here:
You need to actually perform the fetch the first time by calling performFetch: on the NSFetchedResultsController. Either do this in the getter method or it should be done in viewDidLoad.
Have you implemented the required NSFetchedResultsControllerDelegate methods properly? That includes controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, controller:didChangeSection:atIndex:forChangeType: and controllerDidChangeContent:. The code is all boilerplate but you need to make sure it is there.
What do your UITableViewDatasource methods look like? Make sure the sections and row counts look like this:
- (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];
}
Make sure you are using the indexPath to grab your object in the tableView:cellForRowAtIndexPath: method. It should look something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"myCellIdentifier"];
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = rowPackage.title;
return cell;
}
I have a simple app, where I have countries which have cities, which in turn have people. I want to display list of countries in a table view. I use NSFetchedResultsController to get all the data. This is the setup :
-(void)initializeFetchedResultsController
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Country"];
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
fetchRequest.fetchBatchSize = 30;
self.fetchResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[NSManagedObjectContext managedObjectContext]
sectionNameKeyPath:#"nameFirstLetter"
cacheName:nil];
self.fetchResultsController.delegate = self;
[self.fetchResultsController performFetch:nil];
}
I also added an ability to search by typing in the country name in the search bar, so I implemented UISearchBarDelegate method :
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if([searchText isEqualToString:#""])
{
self.fetchResultsController.fetchRequest.predicate = nil;
}
else
{
self.fetchResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name BEGINSWITH[cd] %#", searchText];
}
[self.fetchResultsController performFetch:nil];
[self.tableView reloadData];
}
This works but not the way I want. I expected that on predicate change, my NSFetchResultsControllerDelegate delegate methods will be called again so I can insert or delete items / sections from my table view (i want animations) without having to figure out myself, what has been added and removed. But this is not the case, instead if I change the predicate, delegate methods are not called, and I must do a simple [self.tableView reloadData]. Am I doing something wrong or is it just the way it is supposed to work, and I cannot take this shortcut ?
You are not doing it wrong. This is the way it is and there's no shortcut to take. You'll have to implement your own code to animate the table view between fetches.
As #StianHøiland says, you need to do it yourself. Generally with delegate methods they are called as a result of an 'offline' / asynchronous change in order to notify you. They are not called as a result of a change you have explicitly requested.
You could think about using the fetchedObjects and the indexPathForObject: features of the FRC. Filter the fetchedObjects list (using your predicate). Get the index paths for the objects that have been removed and you can animate them out.