Core data related objects being removed - ios

I'm currently stuck with a weird problem with core data. The app I'm writing downloads a bunch of data from the server, which is translated into core data objects and stored. The device can also create new objects and upload them to the server. One of these objects is a Document which is essentially a representation of a file.
The model for this is MPDocument. A document can also be linked to an MPPlace model, and an MPUser model (users create documents, and documents belong to places).
I'm having no problem downloading the objects from the server, and all relationships are being created and assigned correctly. The problem lies when I try to create a new document on the device itself. The document gets created, and I set all of the relationships, the document gets uploaded and everything seems fine. But when I check the database through a core-data viewer tool, all of the document objects have no value for the place relationship. This happens to all the existing documents, not just the new one. I really can't figure out what's going on!
I'm creating the document like so :
MPUser *current = [MPUser currentUser];
MPDocument *doc = [[MPDocument alloc] init];
doc.name = #"App Upload";
doc.local_url = [NSString stringWithFormat:#"%#", [info valueForKey:UIImagePickerControllerReferenceURL]];
doc.local_url_type = #(MPDocumentUrlTypeAsset);
doc.user = current;
[current addCreatedDocumentsObject:doc];
[doc setValue:self.place forKey:#"place"];
[self.place addDocumentsObject:doc];
I then have a document uploader which handles all uploading :
MPDocumentUploader *uploader = [[MPDocumentUploader alloc] initWithDocument:doc];
uploader.requestDelegate = self;
uploader.successBlock = ^(MPDocumentUploader *uploader, MPDocument *doc) {
NSLog(#"Got doc = %#", doc);
};
[uploader upload];
When the success block is called, the document object DOES have the place relationship set. So even once the upload has finished, the place is set, so I'm really confused now as to where the relationships are being completely cleared.
The document uploader looks something like this :
- (void) upload
{
.... retrieve the local file and turn into NSData. This is fine
MPRequest *request = [MPRequest requestWithURL:_url];
[MPUser signRequest:request];
[request setDelegate:_requestDelegate];
[request setRequestMethod:#"POST"];
[request mountDocumentUploader:self];
[request submit:^(MPResponse *resp, NSError *error) {
if (!error) {
NSDictionary *data = (NSDictionary *)[resp paramForKey:#"data"];
if (data) {
NSLog(#"Document = %#", _document);
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
}
if (_successBlock) {
_successBlock(self, _document);
}
} else {
if (_failBlock) {
_failBlock(self, error);
}
}
}];
}
The MPRequest class handles all the actual uploading and server requests, but doesn't actually touch the MPDocument object.
I can't figure out what's going on or why it's clearing out the relationships. Please can someone help!?
Update
I've played around, and found that the error occurs when the submit block is called. Commenting out
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
works as it is meant to, but now those values obviously aren't set. Adding either of those lines back in 1 at a time in isolation still causes the problem, so it seems that simply editing it at all is breaking it. Still no clue why though :(

ok well I'm not entirely sure what happened or why, but between fiddling with the code, cleaning the project, and restarting my laptop, it seems to have fixed itself so not a clue what the issue was but it currently seems to be ok. Very confused :S

Related

Core Data multithreading--what am I doing wrong

I'll try to keep this brief but basically, I have an app that, in a certain mode, can near-continuously log location and other data, and snap photos (using AVFoundation) and store it all in Core Data. I discovered, as suspected, that all of this would need to be threaded...otherwise the UI gets extremely sluggish.
I have never attempted to combine Core Data with concurrency before so I read up on it as best I could. I feel like I understand what I'm supposed to do, but for someone reason it's not right. I crash with this error: "Illegal attempt to establish relationship "managedDataPoint" between objects in different contexts. I know what this means, but I thought what I have below would avoid this (I'm following what I've read)...since I get an Object ID reference from the main context, and use that to grab a new reference to the object and pass it to the "temp" context...but that isn't working as Core Data still claims I'm attempting to create a relationship across contexts (where?). Appreciate any help. Thank you!
-(void)snapPhotoForPoint:(ManagedDataPoint*)point
{
if (!_imageCapturer)
{
_imageCapturer = [[ImageCapturer alloc] init];
}
if (!_tempContext) {
_tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_tempContext.parentContext = self.managedObjectContext;
}
__block NSManagedObjectID* pointID = [point objectID];
[_tempContext performBlock:^{
NSError *error = nil;
Photo *newPhoto = [NSEntityDescription insertNewObjectForEntityForName:#"Photo" inManagedObjectContext:_tempContext];
UIImage *image = [_imageCapturer takePhoto];
newPhoto.photoData = UIImageJPEGRepresentation(image, 0.5);
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
newPhoto.managedDataPoint = tempPoint; // *** This is where I crash
if (![_tempContext save:&error]) { // I never get here.
DLog(#"*** ERROR saving temp context: %#", error.localizedDescription);
}
}];
}
Shouldn't
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[self.managedObjectContext objectWithID:pointID];
not be
ManagedDataPoint *tempPoint = (ManagedDataPoint*)[_tempContext objectWithID:pointID];
Otherwise you are working with different contexts! Also you should check if objectID is a temporary ID and acquire a "final" one in case of.

MagicalRecord does not save data

I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];

Core data update or create find updated

I have core data entity which contains fields such as name(unique), imageURL and image (saving image as data). Im downloading this data from web API which I have no control of (data in JSON).
I have to check every week if there were changes on API side and update my local database.
Sometimes its changing imageURL property and I have to detect that and download new image and remove old one. Any idea how to implement that (I would be glad for piece of code).
I would have thought it was fairly straight forward.
You are able to download the images when you first get the item.
So now have a check something like...
if currentImageURL is different from newImageURL download the image.
EDIT - To explain how it should work
Assuming you've processed the JSON and now you have an NSArray of NSDictionaries...
You would do something like this...
//I'm assuming the object is called "Person"
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
for (NSDictionary *personDictionary in downloadedArray) {
// You need to find if there is already a person with that name
NSPredicate *namePredicate = [NSPredicate predicateWithFormat:#"name = %#", personDictionary[#"name"]];
[request setPredicate:namePredicate];
// use whichever NSManagedObjectContext is correct for your app
NSArray *results = [self.moc executeFetchRequest:request error:&error];
Person *person;
if (results.count == 1) {
// person already exists so get it.
person = results[0];
} else {
// person doesn't exist, create it and set the name.
person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.moc];
person.name = personDictionary[#"name"];
}
// check the image URL has changed. If it has then set the new URL and make the image nil.
if (![personDictionary[#"imageURL"] isEqualToString:person.imageURL]
|| !person.imageURL) {
person.imageURL = personDictionary[#"imageURL"];
person.image = nil;
}
// now download the image if necessary.
// I would suggest leaving this here and then wait for the image to be accessed
// by the UI. If the image is then nil you can start the download of it.
// now save the context.
}

What should the logic for first run, internet available be?

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.

Why is my app crashing on iPhone 4S because of Core Data request execute?

I have an app that works fine on the simulator 6.1, works fine on the iPhone5 and iPad3 on iOS6.1 but when run on iPhone4S it crashes in this method with Exc Bad Access:
-(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); // if I comment this line out it reaches as far as the next bold line**
// 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);
};
}
//set bool that specifies the coredata has been populated from plist already
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *appFirstStartOfVersionKey = [NSString stringWithFormat:#"first_start_%#", bundleVersion];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:#(YES) forKey:appFirstStartOfVersionKey];
[prefs synchronize];
}
Why only on 4S? It gives no console log and the last know method traversed in this one above. Here is a pic:
And as mentioned above, if I uncomment that NSLog line it reaches as far as logging the array, as shown in the pic. If I leave it in it stops at that line.
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
if (error)
NSLog(#"Error encountered in executing topic fetch request: %#", error);
No no no no no. I know this is a difficult pattern, but please let's try to do it right. Not if (error). error could be anything (esp. under non-ARC). The test is if (!holidayFetchedArray).
For all of these methods that return a value and also take an NSError** by indirection, you test the result to see if it is nil. If it is, then there was an error because returning nil is the sign that there was an error. Then and only then you may touch the error meaningfully.
The docs are always quite clear about this, though it is true that a fog can come over one's eyes at the critical instance, so I've added some comments in italic brackets to call out the key points:
request
A fetch request that specifies the search criteria for the
fetch.
error
If there is a problem executing the fetch, upon return
contains an instance of NSError that describes the problem.
[And if there is no problem executing the fetch, contains garbage so don't touch it!]
Return Value
An array of objects that meet the criteria specified by request
fetched from the receiver and from the persistent stores associated
with the receiver’s persistent store coordinator. If an error occurs,
returns nil. [And that, forsooth, is the sign that an error did occur.] If no objects match the criteria specified by request,
returns an empty array.
This might be the cause of your trouble or it might not, but you must fix it now. I want you to go through all your code looking for NSError* variables declared for use in this pattern and fix all of them! Thank you. Here endeth the lesson.
This is a stab in the dark b/c you haven't identified the line of code where the crash occurs. I recommended you set a breakpoint and then step through until you hit the crash. If what I say below doesn't resolve the issue then edit your post and add more info about where the crash occurs and we'll go from there.
Here's something to check. The line:
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary]
may be crashing if any of the keys in holidayDataDictionary aren't valid for an instance of the Holiday class.
With regard to your question about why it only crashes on your iPhone 4S there's not enough data to explain that yet. Depending on how you've been doing testing and core data migration (if at all) you may have a model inconsistency on that device, though I don't see anything in the screen shot you posted pointing me in that direction. I would try uninstalling/reinstalling your app and see if the crash is still iPhone 4S only.

Resources