I have a NSFetchedResultsController to get the data for an UITableView. During the creation of the NSFetchedResultsController I create a NSPredicate that filters the data with an external condition. What's the proper way to refetch the data? Just nil'ing the my __fetchedResultsController and recreating it seems a little bit brutal.
Thanks!
The initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: documentation states:
Important: You must not modify fetchRequest after invoking this
method. For example, you must not change its predicate or the sort
orderings.
So you have to recreate the FRC (and call reloadData on the table view) when the predicate changes.
EDIT:
The same "NSFetchedResultsController Class References" also states here:
Modifying the Fetch Request
You cannot simply change the fetch request
to modify the results. If you want to change the fetch request, you
must:
If you are using a cache, delete it (using deleteCacheWithName:).
Typically you should not use a cache if you are changing the fetch
request.
Change the fetch request.
Invoke performFetch:.
So my first answer above is probably wrong. If I understand it correctly:
You cannot modify the fetch request assigned to the FRC.
But you can build a new fetch request (with a new predicate) and assign that to the FRC (controller.fetchRequest = newFetchRequest) if you follow the three steps above.
This means that you don't have to recreate the FRC itself.
(I hope that I got it right now :-)
As indicated in my comment above I figured it out. What you need to do is save a reference to the NSFetchRequest and manipulate that straight away when needed. As a second step, you need to tell your NSFetchedResultsController to fetch it's data again.
I did this by adding two new methods to the default NSFetchedResultsController "stack":
- (void)configureFetchRequest {
NSObject *myExternalDependency = …;
if (!__fetchRequest) {
__fetchRequest = [[NSFetchRequest alloc] init];
}
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityName" inManagedObjectContext:self.managedObjectContext];
[__fetchRequest setEntity:entity];
NSPredicate *filter = [NSPredicate predicateWithFormat:#"someValue == %#", [myExternalDependency someProperty]];
[__fetchRequest setPredicate:filter];
}
- (void)performFetch {
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
So what I basically do now is call these two methods on creation of __fetchedResultsController (which I do initialize using __fetchRequest, of course) and every time my external dependency changes. That's it.
Related
There are a lot of questions on this topic but perhaps all too specific and none too concise.
I have a NSFetchedResultsController. It initially fetches the data I need. When I update the data model which would affect the results of the NSPredicate of the NSFetchRequest, the content does not update.
More concretely, I have a Permissions model. There are data objects that are assigned permissions, then there are users who have a subset of these permissions, though the data object's Permission is not the same as the User's permission; they do share the same controlKey.
So, the NSPredicate is:
[NSPredicate predicateWithFormat:#"controlKey IN %#", [[User currentUser].permissions valueForKey:#"controlKey"]];
The problem is, if I create and add a permission, the content does not update when I save the NSManagedObjectContext (yes, I'm observing it. No, I'm not using a cache so nothing to delete).
I'm pretty convinced it's because of the predicate. It's still the same array as initially, and doesn't get updated.
My question is, how do I write a predicate that gets me what I want but still remains "dynamic" ? It would be nice to have UITableView animations as this object is added.
Here is what you need to do. When detecting the change,
self.fetchedResultsController.predicate = //new predicate
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
Or sometimes it is necessary to wipe the FRC clean.
_predicate = // new predicate, put it into an ivar
self.fetchedResultsController = nil;
[self.tableView reloadData]; // lazily re-instantiate FRC with _predicate
This could be expensive, but in order to get animations, you could try replacing reloadData with the following:
[self.tableView beginUpdates];
[self.tableView endUpdates];
You query the control keys from a set of permissions and pass them to the predicate. The predicate itself isn't dynamic anyway, it is only the result of the fetch which is dynamic. So, you can't do exactly what you want.
Instead, you should be observing the permissions change for the user, creating a new predicate, updating the FRC and re-executing it (a requirement after you change the predicate, because it is expected to be static).
I have tried with following code snippet and it works for me:
[NSFetchedResultsController deleteCacheWithName:nil];
[self.fetchedResultsController.fetchRequest setPredicate:predicate]; // set your new predicate
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"%#, %#", error, [error userInfo]);
abort();
}
I have seen this link where countforfetch is always 1. But doesn't give the solution.
When i do a fetch request as given in the link it gives me the data i was about to save every time. So since its already present it wont re-save. So the database is empty. But surprisingly the data comes on the table.
This seems like a very weird behaviour. Can some please help ?
here is my code
NSFetchRequest *fetchRequest12 = [[NSFetchRequest alloc] init];
NSError *error;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"orderNumber = %#",orderList.orderNumber];
[fetchRequest12 setPredicate:predicate];
[fetchRequest12 setEntity:[NSEntityDescription entityForName:#"OrderList" inManagedObjectContext:appDelegate.managedObjectContext]];
NSLog(#"The fetch was successful! %#", [appDelegate.managedObjectContext executeFetchRequest:fetchRequest12 error:&error]);
if ([appDelegate.managedObjectContext countForFetchRequest:fetchRequest12 error:&error] ==0 ) { // Somethings to be done
}
Use setIncludesPendingChanges: to NO if you want the fetch request to ignore any changes that you have made in the MOC but not yet saved. By default all unsaved changes are fetched (hence you see unsaved changes displayed in your UI).
Quite simply I want to iterate through CoreData's [self.fetchedResultsController fetchedObjects] (More specifically I am using MagicalRecord in this example) and change all items that match my criteria (the if statement), then make changes to those objects.
for (Task *aTask in [[self.fetchedResultsController fetchedObjects] mutableCopy]) {
if ([aTask.day past] && [[aTask isArchived] isEqual:#(NO)]) {
NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext];
[context MR_saveOnlySelfWithCompletion:^(BOOL success, NSError *error) {
aTask.day = date;
}];
}
}
But as it turns out it doesn't work! I am trying a solution where I don't have to manually manage everything CoreData as this answer specifies and instead find a succinct solution to the problem, few lines of code and not much iteration.
EDIT: Following from Dan's answer, I have edited my imperfect code to...
NSPredicate *uncompletedTasks = [NSCompoundPredicate andPredicateWithSubpredicates:#[[NSPredicate predicateWithFormat:#"day < %# AND isArchived = %#",[NSDate date],#NO]]];
self.fetchedResultsController = [Task MR_fetchAllSortedBy:#"dateScheduled" ascending:YES withPredicate:uncompletedTasks groupBy:nil delegate:self inContext:[NSManagedObjectContext MR_defaultContext]];
for (Task *aTask in [self.fetchedResultsController fetchedObjects]) {
aTask.day = date;
}
NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext];
[context MR_saveOnlySelfWithCompletion:^(BOOL success, NSError *error) {}];
This solves the problem, although only momentarily when I update the view, I get this error:
CoreData: error: Serious application error. Exception was caught
during Core Data change processing. This is usually a bug within an
observer of NSManagedObjectContextObjectsDidChangeNotification. ***
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects1 with userInfo (null)
I really know nothing about MagicalRecord, but ...
If you iterate through all items fetched by the FRC just to make an update to some of them you better:
1) perform the update in the background
2) fetch only the objects you need updated. example predicate:
NSPredicate* needUpdate = [NSPredicate predicateWithFormat:#"day < %# AND isArchived = %#",[NSDate date],#NO];
NSPredicate* p = [NSCompoundPredicate andPredicateWithSubpredicates:#[FRC_predicate,needUpdate]];
3. perform a single save after you updated all objects (or in batches, don't save one by one)
Guessing ...
You wrote code that make an update to an object (aTask.day = date;) in a completion block.
This might not actually persist the change the way you think it does.
make the update before you call the "save" procedure.
Several problems here.
First, I am not sure why you need mutableCopy. You are not modifying the members of the array yo are iterating through, so this should not be necessary. I am not even sure what the effect of making a managed object copy is in this case. Just leave it out.
Second, your boolean comparison is a bit confusing and can be written in a simpler way. This should be enough:
"... && aTalk.isArchived.boolValue"
Third, you are changing the object in the completion block after the save. So you are not saving the change, just perhaps the previous one made in some earlier iteration through the loop. Instead, just write the change and call save after the loop.
When saving, make sure the used context is the same as that of the fetched results controller.
I have a CoreData entity Tracker which stores the dates.
The app receives a notification and the CheckListViewController enters data in CoreData for up to 13 days, so when the CheckListViewController gets dismissed, the CoreData entity Tracker will be filled with 13 rows.
In the MainViewController (which dismisses CheckListViewController), I have the following code:
- (void)dataSaved {
self.checkListVC dismissViewControllerAnimated:YES completion:^{
// fetching all the data from 'Tracker' entity and doing NSLog on it
// all data gets logged in console without any issues
}];
}
Now, after that somewhere in my code, I fetch all the data from the entity Tracker but the return data is empty. The CoreData doesn't show any error it simply returns and empty array.
Edit:
Code to fetch results from CoreData
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:ENTITY];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"date" ascending:NO];
[request setSortDescriptors:#[sortDescriptor]];
request.predicate = (fromDate && toDate) ? [NSPredicate predicateWithFormat:#"date >= %# AND date <= %#", fromDate, toDate] : nil;
__block NSArray* fetchedHabits;
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
fetchedHabits = [managedObjectContext executeFetchRequest:request error:&error];
if (error) NSLog(#"Unknown error occurred while fetching results from CoreData, %# : %#", error, [error userInfo]);
}];
CoreData model:
Update 1:
So as you can see there are two entities, namely Habit and Tracker. When I fetch results from Habit it all works fine, but when I try to fetch results from Tracker it gives me an empty array. I have a common NSManagedObjectContext instance because you can manage multiple CoreData entities with single managedObjectContext.
I have checked managedObjectContext.persistentStoreCoordinator.managedObjectModel.entitiesByName and it also lists both the entities.
Update 2:
Code where I add data in to Tracker
TrackerCoreData *tracker = [NSEntityDescription insertNewObjectForEntityForName:ENTITY
inManagedObjectContext:managedObjectContext];
tracker.date = date;
tracker.habits = habits;
// saving CoreData explicitly
NSError *error = nil;
[managedObjectContext save:&error];
There could be many reasons for your failure to display the records:
data was not saved
data was not retrieved correctly
data was not displayed correctly
All of these could be potentially complicated scenarios, but you should check them in this order.
A much better approach: use NSFetchedResultsController for your main view controller and have the delegate methods take care of updating your table view. No need to fetch, no work to be done in any completion methods - just save the data and the FRC will update your table.
Edit: how to check the physical database
It is possible that your data only exists in memory but is not actually saved to the database. Find the actual database file (in the documents folder of the app from the Simulator) and check it with the sqlite3 command line utility, or with the Firefox plugin "SQLite Manager".
Edit2: more concrete recommendations
You should make sure that you call:
[managedObjectContext save:&error];
Also double-check what your ENTITY macro stands for (not a very smart name).
It seems to me that you are overusing the block methods to no apparent purpose. First try to make everything work on the main thread (one context!). Only if you get performance problems consider background threads and context and calls to performBlock etc.
Let's say I load in 1,000 objects via Core Data, and each of them has a user-settable Favorite boolean. It defaults to NO for all objects, but the user can paw through at will, setting it to YES for as many as they like. I want a button in my Settings page to reset that Favorite status to NO for every single object.
What's the best way to do that? Can I iterate through every instance of that Entity somehow, to set it back? (Incidentally, is 'instance' the right word to refer to objects of a certain entity?) Is there a better approach here? I don't want to reload the data from its initial source, since other things in there may have changed: it's not a total reset, just a 'Mass Unfavourite' option.
Okay, I've gotten it to work, but it requires a restart of the app for some reason. I'm doing this:
- (IBAction) resetFavourites: (id) sender
{
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
[fetch setEntity: [NSEntityDescription entityForName: #"Quote" inManagedObjectContext: [[FQCoreDataController sharedCoreDataController] managedObjectContext]]];
NSArray *results = [[[FQCoreDataController sharedCoreDataController] managedObjectContext] executeFetchRequest: fetch error: nil];
for (Quote *quote in results) {
[quote setIsFavourite: [NSNumber numberWithBool: NO]];
}
NSError *error;
if (![self.fetchedResultsController performFetch: &error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
}
This works fine if I close and re-open the app, but it isn't reflected immediately. Isn't that what the reloadData should do, cause an immediate refresh? Am I missing something there?