I am wondering if someone could clarify the following from Apples Core Data documentation:
Changes are not reflected until after the controller’s managed object
context has received a processPendingChanges message. Therefore, if
you change the value of a managed object’s attribute so that its
location in a fetched results controller’s results set would change,
its index as reported by the controller would typically not change
until the end of the current event cycle (when processPendingChanges
is invoked). For example, the following code fragment would log
“same”:
NSFetchedResultsController *frc = <#A fetched results controller#>;
NSManagedObject *managedObject = <#A managed object in frc's fetchedObjects array#>;
NSIndexPath *beforeIndexPath = [frc indexPathForObject:managedObject];
[managedObject setSortKeyAttribute:
<#A new value that changes managedObject's position in frc's fetchedObjects array#>;
NSIndexPath *afterIndexPath = [frc indexPathForObject:managedObject];
if ([beforeIndexPath compare:afterIndexPath] == NSOrderedSame) {
NSLog(#"same");
}
What exactly does "would typically not change until the end of the current event cycle" mean? I have this situation in my code but am not really sure if I can 100% rely on my indexes staying the same until I explicitely preform a save on my managed object context. Could the above code be modified, without performing a save to the context, such that it doesn't log "same"?
not really sure if I can 100% rely on my indexes staying the same
until I explicitely preform a save on my managed object context.
I wouldn't. The "current event cycle" is the current iteration of the run loop.
Related
I've scoured and still haven't found anything that quite works. Either the question/answer is too old or it simply hasn't worked for me. This is my first attempt at "my own" app. As it seems a right of passage, I'm making a checklist app. Here's what I'm looking for:
My Data Store contains 4 attributes: name, category, isChecked, isActive (more will surely follow as I expand)
When my View Controller initially loads, the NSFetchedResultsController has an NSPredicate that only retrieves the records whose attribute isActive is YES (or [NSNumber numberWithBool:YES). It then takes those records and displays them into the appropriate cells for the user. When a user clicks on a cell, the Data Store updates and changes the isChecked attribute accordingly. Everything works good to this point.
What I need to do now is to be able to remove the items (1 or more) from the list. Specifically, I need it to update the Data Store attributes isChecked and isActive to NO only if it's current isChecked attribute is YES. (I'm not looking to delete the record from the data store as they will be used to build up the database for the users future use.)
I've used, among other things:
[[[self fetchedResultsController] fetchedObjects]
setValue:[NSNumber numberWithBool:NO]
forKey:#"isChecked"];
This does actually work, it removes the checkmark(s) and updates the store accordingly. Problem is, not only am I making another request to the data store for the isActive items, it also searches the entire "Active List" that was fetched and sets each of their isChecked attributes to NO. This may not be too big of an issue for small lists, but as the list(s) expand this can be an issue.
The other problem is, if I add:
[[[self fetchedResultsController] fetchedObjects]
setValue:[NSNumber numberWithBool:NO]
forKey:#"isActive"];
It sets ALL of my list items to NO (as well as a second data store request within the same method.)
So my question is: How can I get through the list, find only the items that are checked and update only those records (set both the isChecked && isActive attributes = NO) whose isChecked attribute is YES rather than working through the entire list?
I've tried creating a separate fetchedResultsController specifically for this buttons action, and it did work (that is to say, it didn't crash) but the debugger popped out a rather large 'Serious Application Error'. I won't post the error message as it's long and most likely irrelevant to any solution.
Any assistance would be greatly appreciated. Thanks in advance and please be gentle :-].
EDIT
I have tried using a for loop, for (NSString *item in fetchedResultsController) but I get the error ...may not respond to 'countByEnumeratingWithState:objects:count'
It seems a loop of sorts is what's needed here, but again, nothing I can find is relevant or it's outdated. Again, thanks for any assistance.
Edit 2
Here is the original error I got when I ran a second separate fetchRequestController for this button/method:
An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (0 inserted, 3 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
You can just loop over the fetchedObjects collection and change the managed objects. After changing them you'll need to reload your list (I guess you use a tableview).
I don't know what your classes are named, but in general you can just loop over the collection of managed objects and change them. Remember that you need to save your managed object context if you want to keep these changes for when the app closes.
NSArray* myCollection = [[self fetchedResultsController] fetchedObjects];
for(ActiveListData *managedObject in myCollection)
{
if(managedObject != nil && managedObject.isChecked)
{
managedObject.isChecked = NO;
managedObject.isActive = NO;
}
}
If you want to do the check on all object in the database you'll need a new method in your NSFetchedResultsController that has a predicate checking on isChecked and then loops over and edits the result collection.
You might want to post your error code as we could be able to point out what you did wrong.
Edit: If you're not familiar with using Core Data the apple documentation provides a lot of information: http://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdBasics.html
Thanks to #ggfela for his answer. The processes of his answer were spot on. Here is the actual code I put into my button/method, in hopes of it helping someone else in the future:
NSArray *moc = [[self fetchedResultsController] fetchedObjects];
for (ActiveListData *item in moc) {
if (item != nil && item.isChecked.boolValue == 1) {
item.isChecked = [NSNumber numberWithBool:NO];
item.isActive = [NSNumber numberWithBool:NO];
}
}
// Call to Data Store to update the list
NSError *error;
if (![self.managedObjectContext save:&error]) {
FATAL_CORE_DATA_ERROR(error);
return;
Explanation:
Load the contents of the result from calling the fetchedResultsController method into a temporary variable named moc
Use a for loop to cycle through the array of moc. ActiveListData is the NSManagedObject subclass that I created for my Core Data and is the proper place to insert the separated values/attributes from the data store. From there, it's pretty simple, I ensure that item is not nil AND that the item's attribute is the value I need.
NOTE
Core Data does not store the bool values YES and NO but rather 1 and 0, respectively but when you call or compare the values, you simply can not compare the value of item.isChecked because it is being passed back to you as a bool not as an integer. You can not simply compare item.isChecked == YES either since the #property of isChecked is an NSNumber. So, in the case of the if I put item.isChecked.boolValue as this will give me the representing integer for it's bool value, in this case I have it check for a 1 (YES). (Sorry if my explanation is wrong and/or confusing, but this is how I understand it and is the only way this code works.)
Then, setting the new values of these attributes is like you would expect when setting any other variable. The only "tricky" difference with this is that the NSManagedObject subclass sets the #property of the isChecked and isActive to an NSNumber (as mentioned earlier) so in order to send the proper values back to Core Data you use the method numberWithBool of the NSNumber class.
And just in case anyone gets confused by my FATAL_CORE_DATA_ERROR(error) call this is simply a macro that was defined inside the Prefix.pch file to handle my errors from the managedObjectContext. You can use any (or none) error handling you choose.
Thanks again #ggfela for your help!! If anyone else has any other suggestions on how this code should be applied, then please let me know!
You can use NSBatchUpdateRequest to update multiple records
Examples:
https://www.bignerdranch.com/blog/new-in-core-data-and-ios-8-batch-updating/
http://code.tutsplus.com/tutorials/ios-8-core-data-and-batch-updates--cms-22164
I already have a tableView with data in it. IF you tap a cell/row it pushes to an edit type of view. Is there anyway to edit core data's data other than: By edit, i mean i already have data inserted into my context. I have loaded my data into my view, the user can change the existing data, and re save it.
.h
//Below is the entity/entity's class name 'Amm'
Amm *amm;
.m
-(IBAction)save
{
[self.amm setValue:self.nameField.text forKey:#"name"];
[self.amm setValue:self.nicknameField.text forKey:#"nickname"];
[self.navigationController popViewControllerAnimated:YES];
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
}
I want this code to work, however the design pattern of my app isnt allowing this code to work for me as it does in other parts of my app. Thank you very much for any and all help!
I assume from what you've said you have:
A table view listing your managed objects
A view where you can edit the values of a managed object
A save button bound to the save method
What's the actual issue? I'm assuming when you tap save that:
The values in self.nameField.text isn't setting self.amm.name
The values in self.nicknameField.text isn't setting self.amm.nickname
Is that right? If so perhaps try the following code to set the managed object values:
self.amm.name = self.nameField.text
self.amm.nickname = self.nicknameField.text
If that's not the issue and you are actually setting the managed object values properly, is it that you just need to refresh the table-view? Perhaps use some NSLog commands to log every step of the applications progress.
I have this code:
Store* store = [NSEntityDescription insertNewObjectForEntityForName:#"Store"];
store.name = #"My Company"
...
Now the store is managed in the context and will be saved when the context is saved, but I have a button where the user can cancel the form where data is collected. How do I undo or remove this from the context? Or am I thinking wrong?
Core Data has built-in support for undo, so you can undo individual changes by sending the -undo message to the context:
[store.managedObjectContext undo];
It also supports -redo. You can undo all changes up to the most recent save using the -rollback method:
[store.managedObjectContext rollback]
as indicated in #melsam's answer.
As mentioned earlier, you can use an undo manager. Or, you could simply use a separate ManagedObjectContext, and do all your changes in there. If you decide to keep them, save the context. If not, simply discard it. A MOC is just a scratch pad for work, and has no impact on the underlying database until saved.
You can't really "detach an entity" but you can cause a managed object to turn back into a fault, losing any changes that have not been saved.
[managedObjectContext refreshObject:object mergeChanges:NO];
Snipped from the documentation...
If flag is NO, then object is turned into a fault and any pending
changes are lost. The object remains a fault until it is accessed
again, at which time its property values will be reloaded from the
store or last cached state.
[store.managedObjectContext rollback];
Undo works only when I create a undoManager(Swift 5):
managedObjectContext.undoManager = UndoManager()
After this configuration you can undo a last change:
managedObjectContext.undo()
Also you could save all data from the user in an array and when the user is ready, you only have to save the array to core data.
For example:
I have two entities named Projectand Todo where a project has many todos (to-many relationship) and each todo has one Project(see image).
In my ViewController.h I have something like:
...
NSArray *projectArray;
NSArray *todosArray;
#property (nonatomic,retain) NSArray *projectArray;
#property (nonatomic,retain) NSArray *todosArray;
...
In my ViewController.m I have something like:
...
#synthesize projectArray,todosArray;
...
self.projectArray = [self fetchRequestForAllProjects];
...
The user has an interface where he is able to select between all different projects. As soon as the user selects a project, the related todo objects have to be set to be loaded and presented.
Question 1: How do I load the set of todos into the todosArray in the best way?
I was doing it like that (also in the ViewController.m):
...
// after deselecting a project entry I reset the todosArray
self.todosArray = nil;
...
//when the user selects a new project I reset the todosArray like this:
self.todosArray = [selectedProject.todos allObjects];
...
But somehow the app very rarely crashes in the last line of code. Is there any problem with my code?
Question 2: Would it be better to use another fetch request for the todos Objects?
UPDATE:
I am using the todosArrayin various methods of the ViewController.m:
(a) get the count of objects,
(b) present each todos entry inside a table view row, and
(c) to identify the selected todo entry (threw row selection)
Answer #1
It is best to sort them when you pull everything out of the set into an array. This will keep your user experience consistent:
NSSet *projectTodoEntities = [mySelectedProject valueForKey:#"todos"];
NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:#"myKey" ascending:YES];
NSArray *sortedToDos = [projectTodoEntities sortedArrayUsingDescriptors:[NSArray arrayWithObject:sorter]];
Answer #2
No, fetching is expensive compared to just accessing a relationship. A fetch will hit disk, etc.
For answer #1.
1). Please make sure whether selectedProject is deleted on other thread, if it is deleted, core data will mark this NSManagedObject as invalid, when you try to access property of this object, a NSObjectInaccessibleException will be thrown.
2). All NSManagedObject associates NSManagedObjectContext, the context is limited on certain thread or thread queue, when you access "todos" relationship while it is in fault state, it will trigger a fetching from persistent store, you must make sure whether execution thread is valid for NSManagedObjectContext, otherwise you should use below code.
NSManagedObjectContext *context = [selectedProject managedObjectContext];
__weak YouControllerClass *weakSelf;
[context performBlockAndWait:^{
weakSelf.todosArray = [selectedProject.todos allObjects];
}];
Answer #2: Would it be better to use an other fetch request for the todos Objects?
By default the "todos" relationship is returned as fault state, when you access project property "todos:, it actually triggers fetching from persistent store for 1st time, core data may cache these "todos" objects in memory later, so you will get fast access in future (unless you reset NSManagedObjectContext)
For most scenarios, like user checks his limit todo lists, it is ok to trigger another fetch request, the performance is not real problem if there is no huge blob data in todo object.
For performance critical scenarios, like use core data to save hundreds of photos and metadata as a relationship, when you draws all these photos on UIView based on height, width or URL property of photo object, you make consider pre-fetching photo meta to avoid performance hit (io operation).
I'm working on a piece of code for an iPhone application that fetches a bunch of data from a server and builds objects from it on the client. It ends up creating roughly 40,000 objects. They aren't displayed to the user, I just need to create instances of NSManagedObject and store them to persistent storage.
Am I wrong in thinking that the only way to do this is to create a single object, then save the context? is it best to create the objects all at once, then somehow save them to the context after they're created and stored in some set or array? If so, can one show some example code for how this is done or point me in the direction to code where this is done?
The objects themselves are relatively straight forward models with string or integer attributes and don't contain any complex relationships.
In any case, don't save after inserting every object, or be prepared for dreadful performances.
Here is the code I use to populate a Core Data repository upon first launch.
#define MAX_UNSAVED_AIRPORTS_BEFORE_SAVE 1000
int numAirports = 0;
int numUnsavedAirports = MAX_UNSAVED_AIRPORTS_BEFORE_SAVE; // *** bug. see below
for (NSDictionary *anAirport in initialAirports) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Airport *newAirport = [NSEntityDescription insertNewObjectForEntityForName:#"Airport" inManagedObjectContext:managedObjectContext];
newAirport.city = [anAirport objectForKey:#"city"];
newAirport.code = [anAirport objectForKey:#"code"];
newAirport.name = [anAirport objectForKey:#"name"];
newAirport.country_name = [anAirport objectForKey:#"country_name"];
newAirport.latitude = [NSNumber numberWithDouble:[[anAirport objectForKey:#"latitude"] doubleValue]];
newAirport.longitude = [NSNumber numberWithDouble:[[anAirport objectForKey:#"longitude"] doubleValue]];
newAirport.altitude = [NSNumber numberWithDouble:[[anAirport objectForKey:#"altitude"] doubleValue]];
numAirports++;
numUnsavedAirports++;
if (numUnsavedAirports >= MAX_UNSAVED_AIRPORTS_BEFORE_SAVE) {
if (![managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
numUnsavedAirports = 0;
}
[pool release];
}
Also don't forget to save one last time after the loop.
Also be aware that a bug exists that will lead to a crash if all three of the following conditions are met:
The Repository is empty
You have a UITableView with sections
Your first save saves more than one object.
The workaround in the code above is to initialize the numUnsavedAirports to MAX_UNSAVED_AIRPORTS_BEFORE_SAVE in order to make sure the first save happens after the first insert.
I hope this helps.
Saving after each object would produce very bad performance. You should have a balance of the saves perhaps every 100 (testing will determine the sweet spot) and then keep track of where you are at in the processing when the user quits.
You get time on exit to store state so you can easily store your position in the data processing (5 blocks of 100 saved) and pick back up where you left off.
Saving every object individually would hammer the disk and slow the app to a crawl.
It's probably better to create a single object and save the context.
You have 40k objects. Let's say that creating a single NSManagedObject takes x time units. 40kx time units is probably measurable. While the object creation is happening, the user may quit the app for some reason; users are unpredictable. The next time your app starts, you go through the process all over again. It would not be desirable to create the 39,999th object only to have the user quit the app and lose all that work.
If your app were to create each object and save, you could speed up this process a bit. The app starts up and checks to see if it was able to complete the task the last time it ran. If the task was incomplete, it could try to pick up where it left off.
The single object creation and save method may take a longer time to complete but will have a greater likelihood of completing the task.
In terms of memory consumption, this also minimizes the in memory state of your app. The context isn't tracking 40k objects in memory.