Here's what I'm trying to do:
I use NSFetchedResultsController to perform a fetch and track changes using its delegate
I download some data and depending on some condition I sometimes delete all local data stored by CoreData by removing the NSPersistentStore and recreating a new one.
I create managed objects based on the data and save them
NSFetchedResultsController should now inform me that I have some changes
What I get instead is this crash when trying to save the data:
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Object's persistent store is not reachable from this NSManagedObjectContext's coordinator with userInfo (null)
I'm always using a single NSManagedObjectContext and I always read & save on the main thread.
It seems that switching the NSPersistenStore somehow messes up the fetched results controller. Is this expected behavior or am I doing something wrong?
I would not recommend this approach. I would create a new MOC with your new persistent store and let go of the old MOC.
I assume at some point you call -[ManagedObjectContext reset]? Before you do that, you have to let go of all managed objects that come from that context. They all become invalid (which is likely the cause of your crash).
You should also take a look at How to force coredata to rebuild sqlite database model?.
I had the same crash. Following Rob's suggestion, I additionally posted an NSNotification each time I called removePersistentStore: - and all my ViewControllers that have an NSFetchedResultController now automatically nullify their local NSFetchedResultsController when that happens.
i.e.:
1 - LISTEN TO NOTIFICATION:
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Custom initialization
[[NSNotificationCenter defaultCenter] addObserverForName:kNotificationDestroyAllNSFetchedResultsControllers object:nil queue:nil usingBlock:^(NSNotification *note) {
NSLog(#"[%#] must destroy my nsfetchedresultscontroller", [self class]);
[__fetchedResultsController release];
__fetchedResultsController = nil;
}];
}
return self;
}
2 - POST NOTIFICATION
for( NSPersistentStore* store in [self.persistentStoreCoordinator persistentStores] )
{
NSError *error;
NSURL *storeURL = store.URL;
[self.persistentStoreCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error];
/** ... side effect: all NSFetchedResultsController's will now explode */
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDestroyAllNSFetchedResultsControllers object:self];
}
The NSFetchedResultsController monitors changes to objects in its associated managed object context thus it might be monitoring changes to objects in an obsolete managed object context.
This issue got fixed when I recreated the NSFetchedResultsController after resetting the NSPersistentStore
Related
If I call the [self requestData]; from viewDidLoad my table populates itself with data just fine. If I move [self requestData]; to the viewDidAppear method the table remains empty.
Also, I'm not entirely sure if [self.mainTableView reloadData]; is working. I'm trying to move the data request and handling to the viewDidAppear method because I saw that pattern in a code example and thought it might speed up my app launch somewhat. At the moment there's quite a lag from the app launch Default.png to the rootViewController.
thanks for any help with this.
-(void)viewDidAppear:(BOOL)animated
{
[self requestData];
}
-(void)requestData {
[HUD showUIBlockingIndicatorWithText:#"Fetching JSON"];
NSError *requestError = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL
URLWithString:kURL]];
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&requestError];
NSError *jsonParsingError = nil;
if (requestError)
{
NSLog(#"sync. request failed with error: %#", requestError);
}
else
{
// handle data
publicData = [NSJSONSerialization JSONObjectWithData:response
options:0
error:&jsonParsingError];
publicDataArray = [publicData objectForKey:#"data"];
}
/*
for(publicDataDict in publicDataArray) {
NSLog(#"data output is %#",[publicDataDict objectForKey:#"title"]);
}
*/
[self.mainTableView reloadData];
NSLog(#"reload table cat id %#", categoryString);
[HUD hideUIBlockingIndicator];
}
You're trying to optimise the wrong thing.
The reason you have a lag is that your url request is synchronous, so it is blocking the main thread while waiting for the data to be received. Using an asynchronous URL request will give you far better performance benefits that moving the loading call as you are trying to do.
viewDidLoad is the right place to make your JSON request because It is called once, and will keep data into memory.
ViewDidAppear is called each time view appear, it is not really made to retain object
The "lag" is the fact that your UI is not being responsive because the JSON post request is on the main thread.
to speed up launching try to load data asynchronously, like this:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self requestData];
}];
you shoud call it inside viewDidLoad
viewDidLoad is called after the views have been created, but before they're drawn.
viewDidAppear is called after the views have been drawn. It's too late to set data here that affects the display. You could possibly load the data in viewWillAppear, but be aware that it may be called multiple times (e.g. switching between apps).
That's why setting data that affects the display works in viewDidLoad, but not viewDidAppear. viewDidLoad is called only once in a view controller's life cycle.
However, consider the user experience if your user's Internet connection is slow or the server is slow to respond. Your application will seem to "freeze" for awhile.
Instead of a synchronous request, consider using an asynchronous request. You could display a "loading" message, spinner, or something until the request returns, then update the UI directly.
Apple strongly recommends always making asynchronous requests when run from the main UI thread.
There are two entities: Author and Book. Author has an attribute authorName and a to-many relationships books. Book has several attributes and a relationship author. There is a view controller (VCAuthor) to edit an Author object. The child view controller(VCBook) is to edit books of the author. There is only one managedobjectcontext. In the VCBook class i group the undomanager as following
-(void)viewDidLoad
{
NSUndoManager *anUndoManager = [[NSUndoManager alloc] init];
[self.book.managedObjectContext setUndoManager:anUndoManager];
[anUndoManager release];
[self.book.managedObjectContext.undoManager beginUndoGrouping];
}
-(void)cancelAction:(id)sender
{
NSLog(#"%#", self.author.authorName);
[self.book.managedObjectContext.undoManager endUndoGrouping];
[self.book.managedObjectContext.undoManager undoNestedGroup];
self.book.managedObjectContext.undoManager = nil;
NSLog(#"%#", self.author.authorName);
[self dismissModalViewControllerAnimated:YES];
}
the cancelAction is linked to an cancel button on the VCBook which used to undo all the changes made in the VCBook.
Problems is here: First, in the VCAuthor, I edit the authorName in an UITextfiled authorNameTextField from Obama to Big Obama, and save it to the MOC by author.authorName = authorNameTextField.text in the - (void)viewWillDisappear:(BOOL)animated{} method. Then I went into the child view controller VCBook to edit books of the author and click the cancel button to get back to the VCAuthor. I find the authorName still be Obama, that means the expected change of the authorName has been undo. The change of the authorName is not in the undo group at all, and why could this happen? ps. of course i reloadData when i get back into VCAuthor.
I just NSLog the authorName before and after the undo. Before undo the authorName is the changed one Big Obama, and after undo it become Obama
Several things to consider. First, in a scenario like this, I would use a separate MOC instead of the undo manager. Namely, I'd do something like this (assuming ARC - you can do the mapping if necessary)...
You must have some other code providing the book to the VC through a setter, since you access it in viewDidLoad. I'd change viewDidLoad to something like this...
-(void)viewDidLoad
{
self.managedObjectContext = [[NSManagedObjectContext alloc] init];
self.managedObjectContext.parentContext = self.book.managedObjectContext;
// We are in the main thread, so we can safely access the main MOC
// Basically, move our object pointer to book into our local MOC.
NSError * error = nil;
Book *book = [self.managedObjectContext existingObjectWithID:self.book.objectID error:&error];
// handle nil return and/or error
self.book = book;
// Now, your access to book will be in the local MOC, and any changes
// you make to the book or book.author will all be confined to the local MOC.
}
Now, all you have to do is call
[self.managedObjectContext save:&error];
in your saveAndDismiss action. If you don't call save, none of the changes will be saved, they will all just automatically disappear.
EDIT
Note, that the above "save" just moves the object state into the parent context. So, the "main" MOC now has the changes from the "child" but none of the changes have been saved to disk yet.
I am putting the finishing touches on an App, and have difficulties with removing records in bulk. On the hit of a button a set of approx. 3500 records need to be added to the database. This is not a problem, and takes approx. 3-4 seconds.
But sometimes (not often, but the option needs to be there) all these records need to be removed. I just ran this operation, and it took 20 minutes. What could be wrong here? There is only one dependency, all the records are children of a particular Collection.
I add all the items to a set, removed them from the Collection and then delete one by one. Every 5% I update the dialogue and when everything is done I commit the changes. But removing the items just takes ages (as I can see the progress dialogue progress very slowly)
- (void) deleteList:(DOCollection *) collection {
// For the progress dialogue
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Clearing vocabulary list!" forKey:#"message"];
float totalItems = [collection.items count];
float progress = 0;
float nextProgressRefresh = 0.05;
NSMutableSet* itemsSet = [NSMutableSet set];
for (DOItem* item in collection.items) {
[itemsSet addObject:(NSNumber*)[NSNumber numberWithInt:[item.itemId intValue]]];
}
// Remove all of them from the collection
[managedObjectContext performBlockAndWait:^{
[collection setItems:[NSSet set]];
}];
for (NSNumber* itemId in itemsSet) {
DOItem* item = [itemController findItem:[itemId intValue]];
if (item != nil) {
[[self itemController] removeItem:item];
}
progress++;
if((nextProgressRefresh < (progress / totalItems))){
NSString* sProgress = [NSString stringWithFormat:#"%f", (progress / totalItems) * 0.85];
//[dict setValue:#"Saving the database...!" forKey:#"message"];
[dict setValue:sProgress forKey:#"progress"];
[[NSNotificationCenter defaultCenter] postNotificationName:kUpdatePleaseWaitDialogue object:dict];
nextProgressRefresh = nextProgressRefresh + 0.05;
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[managedObjectContext performBlockAndWait:^{
[[self collectionController] commitChanges];
}];
[[NSNotificationCenter defaultCenter] postNotificationName:kSavingDataComplete object:nil];
});
//NSLog(#"Wait 2!");
[NSThread sleepForTimeInterval:1];
}
in DOItemController:
- (void) removeItem: (NSManagedObject*) item {
[[self managedObjectContext] deleteObject:item];
}
Not sure about how you architected your data models. But I would set it up to cascade delete your objects. If the DOItem Objects are unique to the DOCollection, then you can set the delete rule to cascade. That will automagically delete the associated DOItem as well as remove it from the DOCollection item set object.
To delete the DOItem objects from the DOCollection, check your DOCollection.h file, you should have a method along the lines of
-(void)removeDOItemObjects:(NSSet *)value
if not, they may still be dynamically generated by Core Data for you. In your header file you should have something along the lines of:
#property(nonatomic,retain) DOItem *items
then in your implementation file, you should have something along the lines of:
#synthesize items
The appropriate methods that should be generated for these automatically:
-(void)addItemsObject:(DOItem*)value
-(void)addItems:(NSSet *)values
-(void)removeItemsObject:(DOItem *)value
-(void)removeItems:(DOItem *)values
-(NSSet *)items
See "Custom To-Many Relationship Accessor Methods" here for more info.
This method is provided for you when you create the data model and associated implementation files and should be highly optimized by Core Data. Then all you have to do to remove the objects is something along the lines of:
- (void) deleteList:(DOCollection *) collection {
// Remove all of them from the collection
[managedObjectContext performBlockAndWait:^{
// Updated 01/10/2012
[collection removeItems:collection.items];
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Core Data: Error saving context."); }
};
}];
}
You might want to check the performance of the delete using this method and continue to provide the user with feedback. If performance is an issue, consider striding, where you divide the set up into chunks and perform the above method fore each one of the strides, update the user interface, etc.
Again, am not sure about your application architecture, but on first glance, this looks like the issue.
Good Luck!
I am getting the EXC_BAD_ACCESS error when trying to set a value inside a subclass of NSManagedObject for the second time.
I am using zombies but nothing is showing up in the console. Printing out the object using GDB I see that the object has the same memory address both times I try to set the value - not sure why though.
Situation:
I have a view (A) that, when a QR code is scanned, adds a subview (B) which in turn downloads XML that is then saved into a subclassed NSManagedObject.
Inside the subview (B) I navigate back (removeFromSuperView is called)
Back in the original view (A)
Next time, when the same QR code is scanned, it (A) finds the NSManagedObject from the database and attaches that to an instance variable on a new view (same type as B) that it then adds as a subview to the original (A).
In view B's viewDidLoad i always try to set the current date in order to track when a user "saw" that object. This is where I get the EXC_BAD_ACCESS error:
self.currentPiece.piece_last_viewed = [[NSNumber alloc] initWithDouble:[[NSDate date] timeIntervalSince1970]];
Where self.currentPiece is the instance of a subclassed NSManagedObject that was attached in A when that object existed in the database.
I know that it is being released somewhere but I don't know where since managed objects take care of much of that on their own. The error only occurs the second time around that I try to set the value.
I have tried to make this clear. Please tell me if you want me to clarify it even more.
Thanks for the help (have worked on this for some hours now)
UPDATE:
Declaring the piece_last_viewed in HubPiece.h:
#interface HubPiece : NSManagedObject {
}
// ...
#property (nonatomic, retain) NSNumber *piece_last_viewed;
HubPiece.m:
#dynamic piece_last_viewed;
//...inside init method:
self.piece_last_viewed = [[NSNumber alloc] initWithDouble:[[NSDate date] timeIntervalSince1970]];
UPDATE 2:
It is not due to the switching of subviews, that is ruled out. I then realized that I didn't save my changes either, so I introduced save: inside the subclassed NSManagedObject. I then got an earlier error the first time I try to save the entity instance (which saved during an app session, but the data vanishes if I quit the app entirely and then open it up again). So I thought using [context save:&error] would be a good idea :) ...but now that doesn't work and give me a EXC_BAD_ACCESS error.
The HubPiece itself is initialized from another class HubPieceView.m :
self.currentPiece = [[HubPiece alloc] initWithXML:pieceXML];
self.currentPiece is a class variable of type HubPiece and it first declared in .h file and then synthesized in .m file.
Then inside HubPiece.m the initializer looks like this:
-(id)initWithXML:(TBXMLElement *)pieceXML
{
// Setup the environment for dealing with Core Data and managed objects
HenryHubAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityHubPiece = [NSEntityDescription entityForName:#"HubPiece"
inManagedObjectContext:context];
// STORING values
self = [[HubPiece alloc] initWithEntity:entityHubPiece insertIntoManagedObjectContext:context];
// ...setting variables with normal assignment: self.var = value;
NSError *error;
// Save fails
if (![context save:&error] ){
NSLog(#" ERROR: %#", [error localizedDescription]);
}
return self;
}
I just realized my problem. I had been assigning values to the entity through with normal '=' assignment:
self.currentPiece.piece_last_viewed = [[NSNumber alloc] initWithDouble:[[NSDate date] timeIntervalSince1970]];
When it should have been done:
[self setCurrentPiece.piece_last_viewed:[[NSNumber alloc] initWithDouble:[[NSDate date] timeIntervalSince1970]] ];
This is because it is a managed object which creates it's own accessors at runtime through the #dynamic compiler instruction.
I have an app based on the CoreDataBooks example that uses an addingManagedObjectContext to add an Ingredient to a Cocktail in order to undo the entire add. The CocktailsDetailViewController in turn calls a BrandPickerViewController to (optionally) set a brand name for a given ingredient. Cocktail, Ingredient and Brand are all NSManagedObjects. Cocktail requires at least one Ingredient (baseLiquor) to be set, so I create it when the Cocktail is created.
If I add the Cocktail in CocktailsAddViewController : CocktailsDetailViewController (merging into the Cocktail managed object context on save) without setting baseLiquor.brand, then it works to set the Brand from a picker (also stored in the Cocktails managed context) later from the CocktailsDetailViewController.
However, if I try to set baseLiquor.brand in CocktailsAddViewController, I get:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason:
'Illegal attempt to establish a
relationship 'brand' between objects
in different contexts'
From this question I understand that the issue is that Brand is stored in the app's managedObjectContext and the newly added Ingredient and Cocktail are stored in addingManagedObjectContext, and that passing the ObjectID instead would avoid the crash.
What I don't get is how to implement the picker generically so that all of the Ingredients (baseLiquor, mixer, garnish, etc.) can be set during the add, as well as one-by-one from the CocktailsDetailViewController after the Cocktail has been created. In other words, following the CoreDataBooks example, where and when would the ObjectID be turned into the NSManagedObject from the parent MOC in both add and edit cases? -IPD
UPDATE - Here's the add method:
- (IBAction)addCocktail:(id)sender {
CocktailsAddViewController *addViewController = [[CocktailsAddViewController alloc] init];
addViewController.title = #"Add Cocktail";
addViewController.delegate = self;
// Create a new managed object context for the new book -- set its persistent store coordinator to the same as that from the fetched results controller's context.
NSManagedObjectContext *addingContext = [[NSManagedObjectContext alloc] init];
self.addingManagedObjectContext = addingContext;
[addingContext release];
[addingManagedObjectContext setPersistentStoreCoordinator:[[fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
Cocktail *newCocktail = (Cocktail *)[NSEntityDescription insertNewObjectForEntityForName:#"Cocktail" inManagedObjectContext:self.addingManagedObjectContext];
newCocktail.baseLiquor = (Ingredient *)[NSEntityDescription insertNewObjectForEntityForName:#"Ingredient" inManagedObjectContext:self.addingManagedObjectContext];
newCocktail.mixer = (Ingredient *)[NSEntityDescription insertNewObjectForEntityForName:#"Ingredient" inManagedObjectContext:self.addingManagedObjectContext];
newCocktail.volume = [NSNumber numberWithInt:0];
addViewController.cocktail = newCocktail;
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addViewController];
[self.navigationController presentModalViewController:navController animated:YES];
[addViewController release];
[navController release];
}
and here's the site of the crash in the Brand picker (this NSFetchedResultsController is backed by the app delegate's managed object context:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
if ([delegate respondsToSelector:#selector(pickerViewController:didFinishWithBrand:forKeyPath:)])
{
[delegate pickerViewController:self
didFinishWithBrand:(Brand *)[fetchedResultsController objectAtIndexPath:indexPath]
forKeyPath:keyPath]; // 'keyPath' is #"baseLiquor.brand" in the crash
}
}
and finally the delegate implementation:
- (void)pickerViewController:(IngredientsPickerViewController *)pickerViewController
didFinishWithBrand:(Brand *)baseEntity
forKeyPath:(NSString *)keyPath
{
// set entity
[cocktail setValue:ingredient forKeyPath:keyPath];
// Save the changes.
NSError *error;
if (![cocktail.managedObjectContext save:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
// dismiss picker
[self.navigationController popViewControllerAnimated:YES]
}
EVEN MORE
I'm making progess based on Marcus' suggestions -- I mapped the addingManagedObjectContexts to the parent managedObjectContext and wrapped everything in begin/endUndoGrouping to handle cancel vs. save.
However, the object to be created is in an NSFetchedResultsController, so when the user hits the "+" button to add the Cocktail, the (possibly-to-be-undone) entity briefly appears in the table view as the modal add view controller is presented. The MDN example is Mac-based so it doesn't touch on this UI behavior. What can I do to avoid this?
Sounds like you are creating two different Core Data stacks (NSManagedObjectContext, NSManagedObjectModel, and NSPersistentStoreCoordinator). What you want to do from the example is just create two NSManagedObjectContext instances pointing at the same NSPersistentStoreCoordinator. That will resolve this issue.
Think of the NSManagedObjectContext as a scratch pad. You can have as many as you want and if you throw it away before saving it, the data contained within it is gone. But they all save to the same place.
update
The CoreDataBooks is unfortunately a really terrible example. However, for your issue, I would suggest removing the creation of the additional context and see if the error occurs. Based on the code you posted and I assume the code you copied directly from Apple's example, the double context, while virtually useless, should work fine. Therefore I suspect there is something else at play.
Try using a single context and see if the issue persists. You may have some other interesting but subtle error that is giving you this error; perhaps a overrelease somewhere or something along those lines. But the first step is to remove the double context and see what happens.
update 2
If it is crashing even with a single MOC then your issue has nothing to do with the contexts. What is the error you are getting with a single MOC? When we solve that, then we will solve your entire issue.
As for a better solution, use NSUndoManager instead. That is what it is designed for. Apple REALLY should not be recommending multiple MOCs in their example.
I answered a question on here recently about using the NSUndoManager with Core Data but you can also look at some of my articles on the MDN for an example.
Yeah, you definitely don't want to cross context boundaries when setting relationships between objects; they both need to be in the same NSManagedObjectContext. In the old EOF, there were APIs for faulting objects into different contexts, but doesn't look like CoreData has an equivalent.