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.
Related
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.
I recently switched my CoreData backed UITableViews to use a NSFetchedResultsController instead of an NSArray. For one of the tables, scrolling is now very slow, and I think I know why, but I don't know yet what would be the best solution to fix this.
There are two entities Book and Author which are in a many-to-many relationship. Each cell displays the book title, plus the author. If there is more than one author, it will just display the main author. Each Author has an "order" attribute, which is set when the data is imported.
What I have been doing so far is every time the author name is accessed, my Book class returns a mainAuthor property (an NSString):
- (NSString *) mainAuthor
{
if (!mainAuthor)
{
NSSortDescriptor *sortOrder= [NSSortDescriptor sortDescriptorWithKey: #"order" ascending: YES];
NSArray *authorsSorted = [self.authors sortedArrayUsingDescriptors: #[sortOrder]];
Author *a = authorsSorted[0];
mainAuthor = [NSString stringWithFormat: #"%#", a.name];
}
return mainAuthor;
}
For whatever reason this is now called many times instead of only once and causing the slow down. Maybe NSFetchedResultsController fetches the references over and over when scrolling the table?
So how can I fix this? One possibility is to make mainAuthor an attribute instead of a property. So it is set immediately when the data is imported. But before I start messing with my dataModel I'd like to know if this would be the way to move forward, or maybe there is an alternative solution?
UPDATE 1: Here is the code where I set up the fetchController:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSManagedObjectContext *moc = [NSManagedObjectContext MR_defaultContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName: #"Book"];
// sort the books by publishing date
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey: #"date" ascending: YES];
[fetchRequest setSortDescriptors: #[sort]];
// only get the books that belong to the library of the current viewController
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"libraries contains[cd] %#", self.library];
[fetchRequest setPredicate: predicate];
[fetchRequest setRelationshipKeyPathsForPrefetching: #[#"authors"]];
[fetchRequest setFetchBatchSize: 10];
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest
managedObjectContext: moc
sectionNameKeyPath: nil
cacheName: nil];
frc.delegate = self;
_fetchedResultsController = frc;
return _fetchedResultsController;
}
Based on your sample code I'd assume that mainAuthor is not in your Core Data schema. As Core Data handles the lifetime (faulting) of your managed objects you should add this property to your Book entity to avoid unpredictable results by using a transient attribute.
Despite this I'd recommend to return an Author object instead of a NSString as you might change name of the attribute in the future or want to use additional information of the mainAuthor in your UI.
Transient Attribute
Add a transient attribute mainAuthor to your Book's entity and add a custom accessor to your Book's class:
- (Author *)mainAuthor
{
[self willAccessValueForKey:#"mainAuthor"];
Author *value = [self primitiveValueForKey:#"mainAuthor"];
[self didAccessValueForKey:#"mainAuthor"];
if (value == nil)
{
NSPredicate *filterByMinOrder = [NSPredicate predicateWithFormat:#"order == %#.#min.order", self.authors];
value = [[self.authors filteredSetUsingPredicate:filterByMinOrder] anyObject];
[self setPrimitiveValue:value forKey:#"mainAuthor"];
}
return value;
}
The disadvantage of using a transient is that you have to make sure that the data is always up-to-date during the lifetime of the appropriate book. So you have to reset mainAuthor in:
willTurnIntoFault
awakeFromFetch
awakeFromSnapshotEvents:
(Optional, but necessary if the user can change the data)
addAuthorObject:
removeAuthorObject:
by calling [self setPrimitiveValue:nil forKey:#"mainAuthor"].
Hint: Better and faster is to create a synthesized primitiveMainAuthor instead of using primitiveValue:forKey:: Managed Object Accessor Methods
Update
Have you tried to set a fetchBatchSize in your NSFetchedResultsController's fetchRequest? Docs: NSFetchRequest fetchBatchSize
Update 2
Yes, setting the appropriate relationship in setRelationshipKeyPathsForPrefetching is necessary in that case.
To identify bottlenecks it's also really helpful to set the debug argument -com.apple.CoreData.SQLDebug 1 to see the SQL statements created by Core Data. This also often helps to understand the different NSFetchRequest attributes and their impacts.
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.
Got a problem logically deleting an object from table view. Idea is to set a flag to an item like "deleted" to 1 if item shouldn't be shown. Loading data predicate shouldn't load such rows.
Code with predicate and creation of NSFetchedResultController:
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Photo"];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"section" ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES]];
request.predicate = [NSPredicate predicateWithFormat:#"tag = %# and deleted == %#", self.tag, #(0)];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.tag.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
Deletion happens on swipe. Here is action for the swipe:
- (IBAction)deletePhoto:(UISwipeGestureRecognizer *)sender {
CGPoint location = [sender locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (indexPath) {
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.tag.managedObjectContext performBlock:^{
photo.deleted = #(1); //This must trigger data change and view must be redrawn. But it's not happending for some reason.
}];
}
}
The problem here is that row becomes deleted only after application restart. For some reason FetchedResultsController doesn't remove changed value from data and table view still shows the row.
I tried remove the item from table view explicitly but got exception for incorrect number of item in section 0 (that's why I assume that the row is still in fetch result). Explicit call to [self.tableView reloadData] or [self performFetch] don't reload the data and item still there.
I'm almost ran out of ideas what should I do to reload the data.
Thanks in advance.
I shouldn't name a field in DB "deleted". Renamed it and now app works fine.
I'm trying to filter objects in a UITableView, which gets its data from a Core Data sqlite via aNSFetchedResultsController. It's working fine before filtering, but now I need to be able to tap a button and have it display only objects one of whose properties (a BOOL) is YES. Lets call it isFavourite.
I'm a little confused as to where to begin. I understand the concept of a predicate, and I've constructed this one, which should work:
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
However, I'm a little confused as to how to attach that predicate to the NSFetchedResultsController. I already allocated and initialised it, using a separate fetch request which simply returned all objects of that entity (and that works perfectly). If it's been initialised already, can I change its fetch request? If so, how do I do that, then how do I make it re-run the query. Do I then need to manually update the UITableView with reloadData or similar. Then, what do I do when I want to switch this filter off and go back to viewing all results?
I've read a lot of existing questions here but most seem to relate to search bars, which make the answers a lot more complicated. I've also read the Apple documentation, but tend to find its written in a pretty cryptic fashion. My code is below, I'm only part-way through the filterFavourite method and that's where I'm looking for help and clarification.
- (NSFetchedResultsController *) fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[CoreDataController sharedCoreDataController] managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: #"quoteID" ascending: YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize: 50];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest: fetchRequest managedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext] sectionNameKeyPath: nil cacheName: nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (IBAction) filterFavourite: (id) sender
{
if (isDisplayingFavourite) {
// Switch to showing all..
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"isFavourite == 1"];
isDisplayingFavourite = NO;
} else {
// Switch to showing favourites only..
isDisplayingFavourite = YES;
}
}
EDIT: The delegate method "controllerDidChangeContent" is never called in this code. I can't figure out why that is, either. Hugely, hugely confusing, Core Data is like a brick wall. In fact, NONE of the four delegate methods are ever called.
You don't attach a predicate to the NSFetchedResultsController-- you attach it to the NSFetchRequest. It has a method called setPredicate: that does exactly what its name suggests.
You can modify the fetch request by updating the fetchRequest attribute and calling performFetch: again. You could also nil out your fetchedResultsController ivar, and make the code above flexible enough to add whatever predicate(s) would be useful. The next time your fetchedResultsController method is called, it will create a new instance for you with the current predicate(s). That consolidates all of your NSFetchedResultsController code in one method. It's probably slightly more expensive in CPU usage but also probably easier to maintain.