I have a UITableView that I'm populating from Core Data with the following NSfetchedResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:_managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
I have another method attached to an "Add" button that adds a new Core Data item to database. As soon as the object is added, my table updates properly and the new object is shown in its correct spot according to the "date" sort in the fetched results controller. My new object is added using today's date for its "date" attribute. I add the object like this:
NSManagedObject *newEvent = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
[newEvent setValue:#"New Client" forKey:#"name"];
[newEvent setValue:[NSDate date] forKey:#"date"];
NSError *error;
if (![context save:&error]) {
NSLog(#"Core Data error! Could not save: %#", [error localizedDescription]);
}
Now, as part of my "Add" method, I need to select the row where the new item was added and segue to an edit screen. Obviously, depending on the other dates of items in the table, it could be anywhere.
I want to select it like this, but I don't have the indexPath:
[self.eventListTable selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionBottom];
How do I determine which row (indexPath) my new object was added at so that I can select it properly?
Thanks!
NSFetchedResultsController have a method called: indexPathForObject:
If you have inserted items during your change processing (the FRC delegate methods), select the most recent inserted item. you can determine the index path of the object using the method above.
Or, you could keep the inserted object from your last insert, and in the didChangeContent delegate method, select the inserted item and nullify the variable you kept (so further calles won't trigger the segue).
You will need to find the entity in your NSFetchedResultsController to resolve its indexPath. However to do that, you need to wait for your NSFetchedResultsController to recognize the object. You will probably need to wait for the delegate callback methods from the NSFetchedResultsController to fire and then use -indexPathForObject: to resolve it back to an indexPath and then select the row.
Visually it should work perfectly. The row will appear and then get selected.
Related
I'm trying to use NSFetchedResultsController to display data in a table view. My data model has an array of users, each user has an array of categories, each category has an array of organizations, and each organization has an array of accounts. The table view displays data from a single user. Each row represents an organization belonging to the user, and the organizations are separated into sections, with each section containing the organizations belonging to a specific category. In my app delegate I populate my application with some dummy data, and NSFetchedResultsController displays that data fine. The problem is that when I try to add a new organization to an existing category the row is added but it is added to a new section containing only that row, as opposed to merging with the section that holds the rest of the organizations for that category.
Here is the code for my NSFetchedResults controller:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PKOrganization" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Tell the fetch request to only retrieve the organizations from the proper user
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:[NSString stringWithFormat:#"(category.user.name = '%#')", self.user.name]]];
// Sort the data
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"category.name" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor1,sortDescriptor2];
[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:#"category" 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;
}
It might also be worth noting that I am able to tell that the data is added as expected. It just doesn't get displayed correctly.
Try changing this line
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"category" cacheName:#"Master"];
To this:
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"category.name" cacheName:#"Master"];
In my experience, I needed the sectionNameKeyPath to be exactly the same as the first sortDescriptor in order to get sections to work properly.
I have a strange bug: if I uncomment my NSPredicate, the resulting UITableView is empty.
My data Model is the following:
Category <-->> Feed <-->> Post
I am fetching the Posts. Post.feed is a Post's Feed. Feed has an rss NString property.
Here's the code:
- (NSFetchedResultsController *)fetchedResultsController {
// Set up the fetched results controller if needed.
if (_fetchedResultsController == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Post"
inManagedObjectContext:_globalMOC];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"feed.rss == %#", _detailItem.rss];
[fetchRequest setPredicate:predicate];
// 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:_globalMOC
sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
self.fetchedResultsController.delegate = self;
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. If it is not possible to recover from the error, display an alert
// panel that instructs the user to quit the application by pressing the Home button.
//
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
return _fetchedResultsController;
}
As I told before, I only see results if I uncomment the NSPredicate. I tried with LIKE, ==, =, with double and single quotes around %#...
BTW, The best would be to directly compare the Feed object...
According to Apple, the syntax might not be the issue, but then what?
The Posts are created in a separate ManagedObjectController sharing the same PersistentStoreCoordinator. I get the required Feed's objectID in order to associate the new Post with its corresponding Feed in the child MOC (otherwise I'd get an error regarding associating objects from different MOC).
I also duely merge my MOCs in the main thread whenever the child MOC notifies it of a change.
Basically: if I NSLog the Posts I have (commented-NSPredicate), I see every Post with the relevant RSS Feed URL fitting the displayed Feed (= detailItem).
Anyone can help me?
If your NSFetchedResultsController is blank then it's pretty sure that you're getting no results through the fetch request and that i'm afraid, because of inappropriate predicate statement or no matching records. i guess the problem is due to presence of wildcard(don't know much about that)
check NSPredicate Class Reference and Predicate Programming Guide to get accurate results through predicates.
Eureka!
The problem was the following: when I create my NSFetchedResultsController in my DetailView, _detailItem is nil.
So, even after when setting _detailItem, the NSPredicate still focus on comparing my feed relationship to a nil object.
I solved the problem by refreshing my NSFetchedResultsController.fetchRequest in the didSelectRowAtIndexPath in the MasterView the following way:
Feed *feed;
if (tableView == self.searchDisplayController.searchResultsTableView) {
feed = [_filteredCategoryArray objectAtIndex:indexPath.row];
} else {
feed = [[self fetchedResultsController] objectAtIndexPath:indexPath];
}
self.detailViewController.detailItem = feed;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"feed == %#", feed];
[self.detailViewController.fetchedResultsController.fetchRequest setPredicate:predicate];
Hope this solution might help other people.
Thanks for your help, nickAtStack!
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.
Scenario :
I have an expense tracking iOS Application and I am storing expenses from a expense detail view controller into a table view that shows the list of expenses along with the category and amount.
On the top of the tableview, is a UIView with CALENDAR button, a UILabel text showing the date (for example: Oct 23, 2012 (Sun)) and 2 more buttons on the side.
The pressing of the calendar button opens up a custom calendar with the current date and the two buttons are for decrementing and incrementing the date correspondingly.
I want to save the expenses according to the date which is an attribute in my Core data entity "Expense".
Question: Suppose I press the calendar button and choose some random date from there, the table view underneath it, should show that day's particular expenses. What I mean is I want the table view to just show a particular date's expenses and if I press the button for incrementing the date or decrementing the date, the table view should show that day's expenses. I am using NSFetchedResultsController and Core Data in order to save my expenses.
Any thoughts on how I would achieve this? Here's the code for FRC.
-(NSFetchedResultsController *)fetchedResultsController
{
if(_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
NSFetchRequest * request = [[NSFetchRequest alloc]init];
[request setEntity:[NSEntityDescription entityForName:#"Money" inManagedObjectContext:context]];
NSSortDescriptor *sortDescriptor1 =
[[NSSortDescriptor alloc] initWithKey:#"rowNumber"
ascending:YES];
NSArray * descriptors = [NSArray arrayWithObjects:sortDescriptor1, nil];
[request setSortDescriptors: descriptors];
[request setResultType: NSManagedObjectResultType];
[request setIncludesSubentities:YES];
[sortDescriptor1 release];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController.delegate = self;
[request release];
NSError *anyError = nil;
if(![_fetchedResultsController performFetch:&anyError])
{
NSLog(#"error fetching:%#", anyError);
}
return _fetchedResultsController;
}
Thanks guys.
You would have to create a new NSFetchedResultsController with a new NSFetchRequest that has an appropriately set NSPredicate:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(date == %#)", dateToFilterFor];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Expense" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPredicate:predicate];
// ...
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"SomeCacheName"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
Don't forget to call [self.tableView reloadData]; after assigning the new FRC.
Edit:
You can assign a predicate to an NSFetchRequest which then is assigned to the fetchedResultsController. You can think of the predicate as a filter.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(date == %#)", dateToFilterFor];
If you add this to the fetch request by calling [fetchRequest setPredicate:predicate]; you tell the fetched request to only fetch results where to date property of the NSManagedObject matches the date you provide in the predicate. Which is exactly what you want here.
So if you have a method that's called after the user selected a date you could modify it like this:
- (void)userDidSelectDate:(NSDate *)date
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
//Here you create the predicate that filters the results to only show the ones with the selected date
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(date == %#)", date];
[fetchRequest setPredicate:predicate];
// 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];
[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;
//Here you replace the old FRC by this newly created
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();
}
//Finally you tell the tableView to reload it's data, it will then ask your NEW FRC for the new data
[self.tableView reloadData];
}
Notice that if you're not using ARC (which you should) you'd have to release the allocated objects appropriately.
I know how to sort Core Data objects in a tableview by NsDate, but this by default seems to create a new section for each object. I want to sort them by a medium formatted date with NSDateFormatter. How would I do this?
For example, if I have 3 objects created on the same day, I want them to be in the same section with the section title being that Day, no time needed.
Each object has an NSDate property. Thanks for your help.
This is the code I have in fetchedResultsController with rgeorge's suggestions. What am I missing here?
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
NSLog(#"get old fetched controller");
return fetchedResultsController;
}
else{
NSLog(#"get new fetched controller");
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"InTextEntity" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *dateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dateModified" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:dateDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"mediumFormattedDate" cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return fetchedResultsController;
}
(I'll write this up assuming you're using an NSFetchedResultsController to drive your tableview. If you're not, I recommend checking it out.)
An interesting feature of NSFetchedResultsController's sectioning abilities: although the property you sort on must be a modeled property (because sqlite does the actual sorting), the property you group the sections with need not be. The only requirement is that the grouping be consistent with the ordering. (i.e., sorting by the sort property will put the objects with matching group properties next to each other.)
So just add something like this to your modeled object class:
// in interface
#property (nonatomic, readonly) NSString *mediumFormattedDate;
// in impl
-(NSString *)mediumFormattedDate
{
// this can be fancier if you need a custom format or particular timezone:
return [NSDateFormatter localizedStringFromDate:self.date
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterNoStyle];
}
(no need to mention mediumFormattedDate in the .xcdatamodel at all.)
Then go ahead and sort your objects by the date property, but group them by your new property. When you create your NSFetchedResultsController, do so along these lines:
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:#"MyFancyEntity"];
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:#"date"
ascending:YES];
[fr setSortDescriptors:[NSArray arrayWithObject:sd]];
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc] initWithFetchRequest:fr
managedObjectContext:myManagedObjectContext
sectionNameKeyPath:#"mediumFormattedDate"
cacheName:nil];
// then do stuff with frc
That's all it takes! I've done this in a few apps to get date grouping and it works well.
Sounds like you're setting the section index on the fetched results controller to be your date property, which seems undesirable.
Instead you should probably be computing the section index yourself, and sorting by date. You can accomplish this in either your data model or by computing the sections manually in code.
For example, you could add a property to your managed object model called "Day" and set that to whatever value you want to use (you don't specify if its something like Monday or an actual date like 21).
You can then pass that property to the fetched results controller.
Alternatively you could implement the sections yourself, days are easy, its Monday-Sunday. Dates are a bit harder, 1-28,30,31 depending on what month it is. Then use an appropriate NSPredicate / NSFetchRequest to get the count of the items in each section.