For the life of me I can't work this one out, but CoreData keeps throwing me an error.
Cannot delete object that was never inserted.
Here is the jist of my app cycle:
1/ Push ViewController.
2/ Get managed object context from app delegate.
FLAppDelegate *appDelegate = (FLAppDelegate *)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = appDelegate.managedObjectContext;
3/ Check if Session exists.
4/ No Session exists, create a new one.
self.session = nil;
self.session = [NSEntityDescription insertNewObjectForEntityForName:#"Session" inManagedObjectContext:self.managedObjectContext];
//Set attributes etc...
//Keep a reference to this session for later
[[NSUserDefaults standardUserDefaults] setURL:self.session.objectID.URIRepresentation forKey:kKeyStoredSessionObjectIdUriRep];
[[NSUserDefaults standardUserDefaults] synchronize];
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
//Handle error if save fails
}
5/ Pop ViewController.
6/ Return to ViewController.
7/ Again, check if Session exists.
8/ A Session is found! (By looking at NSUserDefaults for the one we stored to later reference). So I get Session I created earlier then give the user a choice to delete that one and start fresh or continue with that one.
NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:kKeyStoredSessionObjectIdUriRep];
if (url) //Found existing flight session
{
NSManagedObjectID *objId = [self.managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:url];
NSManagedObject *obj = [self.managedObjectContext objectWithID:objId];
self.session = (Session *)obj;
//Ask the user if they want to continue with this session or discard and start a new one
}
9/ Choose to delete this Session and start a new one.
10/ Problem begins here! I delete the reference I am keeping to this Session as it is no longer relevant and try to delete the object then save those changes.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kKeyStoredSessionObjectIdUriRep];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.managedObjectContext deleteObject:self.session];
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
//Error!
}
11/ There we have it, when I try to run the save method, it crashes and throws an error: NSUnderlyingException = "Cannot delete object that was never inserted.";
I have no clue why it says so, as I appear to save the object whenever I create one and create the reference, retrieve the object from that reference but then deleting just breaks everything.
The problem is in your step 4. In this step you
Create the Session
Save its managed object ID to user defaults
Save changes to your managed object context.
The reason this fails is that when you create a new managed object, it has a temporary object ID that is only valid until you save changes. As soon as you save changes, it gets a new, permanent object ID. You're saving the temporary object ID, but when you look it up later it's not valid any more.
You can fix this just by changing the order of operations in step 4 so that you:
Create the Session
Save changes to your managed object context
Save the Session's object ID to user defaults
This way you'll have a permanent ID when you save the value to user defaults.
Related
I have an NSManagedObject (User) in database. Then I'm trying to fetch that object from database and update field firstName:
NSFetchRequest *fetchR = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSError *err = nil;
NSArray *allUsers = [self.managedObjectContext executeFetchRequest:fetchR error:&err];
TMUser *profile = allUsers.firstObject;
[profile setValue:#"Username" forKey:#"firstName"];
[self.managedObjectContext save:&err];
if (err) {
NSLog(#"Error: %#", err.localizedDescription);
}
The code passes without errors. But if I relaunch my app, fetch request retunrs user without updated field "firstName". I have only 1 NSManagedObjectContext. All Core Data stack was initialized successfully. After fetch my user is:
Printing description of allUsers:
<_PFArray 0x14ed6600>(
ID:3451
firstName:Johnatan
lastName:Hike
phone:380995046960
email:igor#email.com
language:en
)
For some reason object changes wasn't registered in context(Context hasChanges = NO before save). What am I doing wrong? Please, help
I think you are not saving the master context.
Please check that you call:
[managedObjectContext save:&error];
on all child contexts that save the data,
and after that on the master context as well.
You have one global function(in AppDelegate) saveContext which saves everything and which I can call from anywhere safely.
I solved my problem. I recreated NSManagedObject subclass from xcdatamodeld scheme and it works. I found that if I add another properties(readonly etc.), not related to data model scheme or change property type from NSNumber(aka bool) to BOOL, it stops updating existed objects in database.
changesOperation.fetchRecordChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError){
//encode and save token
NSData *encodedServerChangeToken = [NSKeyedArchiver archivedDataWithRootObject:serverChangeToken];
[[NSUserDefaults standardUserDefaults] setObject:encodedServerChangeToken forKey:fetchToken];
[[NSUserDefaults standardUserDefaults] synchronize];
//handle more - **this causes a retain cycle**
if(changesOperation.moreComing){
}
};
Hi just wondering in the fetchRecordChangesCompletionBlock, the docs say:
If the server is unable to deliver all of the changed results with this operation object, it sets this property to YES before executing the block in the fetchRecordChangesCompletionBlock property. To fetch the remaining changes, create a new CKFetchRecordChangesOperation object using the change token returned by the server.
In the code above this causes a retain cycle so how should this be handled and when recreating the operation is it possible to use the same completion blocks alreay created?
You should define a weak changesoperation like this
__weak CKFetchNotificationChangesOperation *weakChangesOperation = changesOperation;
changesOperation.fetchRecordChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError){
...
if(weakChangesOperation.moreComing){
}
I have followed a variety of posts here in SO to delete all the data from an app so I can start over. I have tried:
A) Deleting all the data:
NSArray *entities = model.entities;
for (NSEntityDescription *entityDescription in entities) {
[self deleteAllObjectsWithEntityName:entityDescription.name
inContext:context];
}
if ([context save:&error]) {
...
- (void)deleteAllObjectsWithEntityName:(NSString *)entityName
inContext:(NSManagedObjectContext *)context
{
NSFetchRequest *fetchRequest =
[NSFetchRequest fetchRequestWithEntityName:entityName];
fetchRequest.includesPropertyValues = NO;
fetchRequest.includesSubentities = NO;
NSError *error;
NSArray *items = [context executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *managedObject in items) {
[context deleteObject:managedObject];
NSLog(#"Deleted %#", entityName);
}
}
B) Delete the physical data store:
NSError *error;
NSPersistentStore *store = [[self persistentStoreCoordinator].persistentStores lastObject];
NSURL *storeURL = store.URL;
NSPersistentStoreCoordinator *storeCoordinator = store.persistentStoreCoordinator;
[self.diskManagedObjectContext reset]; // there is a local instance variable for the disk managed context
[storeCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error];
_diskManagedObjectContext = nil;
C) Perform step A and then step B
In all combinations it appears to run with no errors, but whenever I receive new data (via my HTTP service) and start adding it to the re-initialized data store I get all kinds of duplicate data and various data issues. I usually have to delete and reinstall the app to get the data clean enough to re-initialize.
It should be fairly straightforward. The user logs in. App data is downloaded and saved in the store. User logs out and logs in again or as different ID and new data is brought down.
Any ideas why the above methods are not working?
UPDATE:
I edited my code above to show that I am saving the context and removing the data store file. I still end up with bad leftover data. Could the problem be the multiple contexts we use? We have three contexts we use in the app: a UI-managed context, a background context and a disk-managed context. A notification listener takes care of merging changes in the background context with the disk managed context.
I have tried altering the above code to loop through the objects in all three contexts and we set them all to nil. The authentication code takes care of reinitializing the contexts. Still banging my head on what seems like a simple issue.
After
for (NSEntityDescription *entityDescription in entities) {
[self deleteAllObjectsWithEntityName:entityDescription.name
inContext:context];
}
Save your context
[context save:&error];
(B) doesn't delete the physical store, it just dissociates it from your app for the time being. No doubt you just attach it again shortly thereafter or upon next launch.
Use [[NSFileManager defaultManager] removeItemAtURL:... error:...] actually to delete the file from your disk.
As the other posters have said, you fail to NSManagedObjectContext -save: in (A) so you affect what's in that one context but not in the persistent store. Contexts are just in-memory scratch pads so as soon as you create a new context it'll be able to find everything in the persistent store again unless or until you save the one with the modifications.
I import the logged in user's data from server into a Core Data Entity called "User". I also keep a reference of this specific User object onto my AppDelegate (as a property) so I can access it elsewhere in my app. The problem I am facing is, when I push another view controller and try to access appdelegate.loggedInUser.id , I see that "id" is nil. Debugger shows this for the object :
$24 = 0x0b28ad30 <User: 0xb28ad30> (entity: User; id: 0xb261160 <x-coredata:///User/tC48E8991-B8A6-4E68-9112-93F9F21DB5382> ; data: <fault>)
My understanding was that the Core Data framework would fire the fault the moment I try to access one of the properties of this object. I am confused as to why me accessing the "id" property of the user is not firing a fault in this case?
EDIT:
This is how create and use the loggedInUser object :
//method to get bgContext
+(NSManagedObjectContext *)getContextOnBgWithParentSetToMainMOC
{
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[tmpContext setParentContext:[Utils getAppDelegate].managedObjectContext];
return tmpContext;
}
//in App Delegate
NSManagedObjectContext *bgContext = [NSManagedObjectContext getContextOnBgWithParentSetToMainMOC];
self.loggedInUser = [User importFromObject:loggedInUserData inContext:bgContext completionBlock:^(NSManagedObjectContext *theContext, NSManagedObject *theManagedObjectWithValuesImported) {}];
//In User.m file
+ (User *)importFromObject:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context completionBlock:(TemporaryContextImportReturnBlock)block {
if ( !context ){
context = [NSManagedObjectContext getContextOnBgWithParentSetToMainMOC];
}
NSManagedObjectContext *localContext = context;
User *newUserEntity = [NSEntityDescription insertNewObjectForEntityForName:#"User" inManagedObjectContext:localContext];
NSArray *emailsArray = [dictionary objectForKey:#"emails"];
NSString *emailsString = #"";
if ([emailsArray count] > 0){
emailsString = [emailsArray componentsJoinedByString:#","];
}
newUserEntity.emails = emailsString;
newUserEntity.id = [dictionary objectForKey:#"id"];
newUserEntity.n = [dictionary nonNullObjectForKey:#"n"];
return newUserEntity;
}
//Access in one of the view controllers
User *loggedInUser = [Utils getAppDelegate].loggedInUser;
// loggedInUser.id /*nil*/
I have the same problem. It turns out, according to this answer, which references the Apple docs, that an NSManagedObject does not hold a strong reference to its NSManagedObjectContext as you might expect. I suspect that if you inspect your object when it doesn't fire the fault properly that [myObject managedObjectContext] == nil.
I don't know what best practices are here. The obvious (but potentially difficult) solution is to find out why your MOC is being deallocated. As an alternative, although I'm unsure whether it's safe to do, you could retain the MOC from each NSManagedObject instance. (I have question about that open here.)
make sure, that you do not call
[managedObjectContext reset];
somewhere. From Apple doc:
Returns the receiver to its base state.
All the receiver's managed objects are “forgotten.” If you use this method, you should ensure that you also discard references to any managed objects fetched using the receiver, since they will be invalid afterwards.
Those "orphaned" managed object's managedObjectContext property will change to nil and they will not be able to fire faults anymore.
For an app that fetches web from a web service, I have included a plist to be parsed into CoreData if its the first run because the data is not readily available in the Docs directory or may take long to fetch from the web. I do have NSNotifications signaling when a web fetch/synchronization has succeeded though.
At present in AppDelegate applicationDidFinishLaunchingWithOptions I call:
[self checkIfFirstRun];
which is this:
-(void)checkIfFirstRun{
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *appFirstStartOfVersionKey = [NSString stringWithFormat:#"first_start_%#", bundleVersion];
NSNumber *alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] objectForKey:appFirstStartOfVersionKey];
if(!alreadyStartedOnVersion || [alreadyStartedOnVersion boolValue] == NO) {
// IF FIRST TIME -> Preload plist data
UIAlertView *firstRun = [[UIAlertView alloc] initWithTitle:#"1st RUN USE LOCAL DB"
message:#"FIRST"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[firstRun show];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:[NSNumber numberWithBool:YES] forKey:appFirstStartOfVersionKey];
[prefs synchronize];
//Use plist
[self parsePlistIntoCD];
} else {
UIAlertView *secondRun = [[UIAlertView alloc] initWithTitle:#"nTH RUN WEB FETCH"
message:#"nTH"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[secondRun show];
}
}
So ok, i get my plist parsed perfectly into my CoreData db.
Here is the parsePlistIntoCD:
-(void)parsePlistIntoCD{
self.managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// 3: Now put the plistDictionary into CD...create get ManagedObjectContext
NSManagedObjectContext *context = self.managedObjectContext;
NSError *error;
//Create Request & set Entity for request
NSFetchRequest *holidayRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *topicEntityDescription = [NSEntityDescription entityForName:#"Holiday" inManagedObjectContext:context];
[holidayRequest setEntity:topicEntityDescription];
//Create new NSManagedObject
//Holiday *holidayObjectToSeed = nil;
Holiday *newHoliday = nil;
//Execute fetch just to make sure?
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
if (error) NSLog(#"Error encountered in executing topic fetch request: %#", error);
// No holidays in database so we proceed to populate the database
if ([holidayFetchedArray count] == 0) {
//Get path to plist file
NSString *holidaysPath = [[NSBundle mainBundle] pathForResource:#"PreloadedFarsiman" ofType:#"plist"];
//Put data into an array (with dictionaries in it)
NSArray *holidayDataArray = [[NSArray alloc] initWithContentsOfFile:holidaysPath];
NSLog(#"holidayDataArray is %#", holidayDataArray);
//Get number of items in that array
int numberOfTopics = [holidayDataArray count];
//Loop thru array items...
for (int i = 0; i<numberOfTopics; i++) {
//get each dict at each node
NSDictionary *holidayDataDictionary = [holidayDataArray objectAtIndex:i];
//Insert new object
newHoliday = [NSEntityDescription insertNewObjectForEntityForName:#"Holiday" inManagedObjectContext:context];
//Parse all keys in each dict object
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary];
//Save and or log error
[context save:&error];
if (error) NSLog(#"Error encountered in saving topic entity, %d, %#, Hint: check that the structure of the pList matches Core Data: %#",i, newHoliday, error);
};
}
[[SDSyncEngine sharedEngine] startSync];
}
The thing is, I need to also make sure that if there is internet available, that my CoreData db get repopulated with the fetched web data.
But If I leave the call to [self parsePlistIntoCD]; only the plist data is present in the CoreData. First or nth run, I only get the plist data. If I comment that line out, I get my web fetched data.
Why doesnt the web fetched data replace the plist parsed data?
So the logic of parsePlistIntoCD is essentially
if no objects in store, load them from plist
always invoke startSync on [SDSyncEngine sharedEngine], which handles the web download and sync.
It looks to me like your startSync will in fact be invoked. So I would look there for the bug. You could add a log statement, or set breakpoints, to verify that that code path is actually being followed.
Both the plist parse and the web data fetch might take some time. That's a sign that you should be doing these operations in the background, perhaps with a GCD queue. You don't know in advance whether either of them will succeed. So don't set the preferences until they finish.
Side note: you can query the preferences database for BOOLs, making your code shorter, and therefore easier to read.
BOOL alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] boolForKey:appFirstStartOfVersionKey];
and
[prefs setBool:YES forKey:appFirstStartOfVersionKey];
You can also replace numberWithBool: with simply #(YES) and #(NO).
For your program logic, I suggest something like this:
In -applicationDidFinishLaunchingWithOptions:, check to see if the starting plist data has been loaded. Forget about whether it's the first run. Just see whether the plist data needs to be loaded. Maybe call that shouldLoadPlistData. Or maybe you need to tie that to the version you're running, in which case you'd store a string latestPlistVersionLoaded.
If you haven't loaded it yet, enqueue a block to perform the plist load. At the conclusion of the plist load, set shouldLoadPlistData to NO, to note that plist data no longer needs to be loaded. If, for some reason, the plist load fails (maybe the phone runs out of battery or your app is killed by user or system), then on the next launch you're back where you started.
also check to see whether you have net access. If you do, enqueue a block to retrieve the web-based data, parse the data, and then, upon conclusion, update the preferences.
If the data is large, you might want to checkpoint this work:
Do I have the full web update? Then I'm done. Otherwise...
Has the download finished? Yay, I have the data, let's load it.
If not, have I started the download?
This staged checkpointing will also allow you to ask the system for extra time, if your app exits in the middle of the download.
parseListIntoCD feels a bit bloated to me. It does more than its name implies. Perhaps you could refactor it into a check (shouldLoadPlist), a method that does the import (importPlist:intoContext:), and a method that fires off the sync.
I strongly suggest that you pass the working NSManagedObjectContext in as a parameter, rather than having some global object that dispenses MOCs (as [SDCoreDataController sharedInstance] appears to do. It gives you much more control, and allows you to write unit tests much more easily. If you also pass in the path to the plist, you now have clean code that should behave the same way every time you call it.
Your use of the NSError ** parameter is consistently incorrect. The value of NSError is undefined upon success. You must test the result of the operation, not the value of the error, to determine whether you succeeded. The idiom is always
if (![someObject doTaskWithObject:foo error:&error]) {
// handle the error
}
Take a look also at countForFetchRequest:error. It would give you the same info that you're currently extracting by performing a fetch and counting results, but without having to instantiate the NSManagedObjects.