iOS Core Data to-many relationship insert/fetch - ios

In my application I have trip, which has many stops. This has been specified in the .xcdatamodeld file. I can go ahead an manipulate trips in any manner that I want and they all work okay. However, I am running into issues with adding many stops to each trip. It does not seem to persist. Here is the following code. this code is inside of a a detailViewController of a trip, so a specific trip has already been clicked on.
Trip * trip = [self retrieveObjectWithID:_passedObjectId];
StopOff *newStop = [NSEntityDescription insertNewObjectForEntityForName:#"StopOff" inManagedObjectContext:managedObjectContext];
[newStop setValue:stopName forKey:#"stopName"];
[newStop setValue:stopCity forKey:#"stopCity"];
[newStop setValue:stopDate forKey:#"stopDate"];
[newStop setValue:stopAddress forKey:#"stopAddress"];
[newStop setValue:stopState forKey:#"stopState"];
[newStop setValue:stopTime forKey:#"stopTime"];
newStop.trip = trip;
[trip addStopObject:newStop];
NSLog(#" stop count %i", [trip.stop count]);
Here is what my console spits out the first time i hit a button to run this: CoreData: annotation: to-many relationship fault "stop" for objectID 0x8bb2810 <x-coredata://4CE70783-4729-46E0-B18B-8E325D1020CC/Trip/p20> fulfilled from database. Got 0 rows 2013-11-24 21:19:43.417 Tracker[30633:70b] stop count 1
If I keep hitting the button, stop count increases. If I relaunch the app the stop count goes back down and starts over again, so it seems that it is not persisting.
My question is, how exactly do I insert many stops that correspond to a trip, and once they are inserted and persist, how do I go ahead and get every corresponding stop to that trip.
Here is the code for which retrieves each trip just fine. and managedObjectContext is handled in the parent view controller using NSFetchedResultsController. Please let me know if you need anymore information
- (Trip*)retrieveObjectWithID:(NSManagedObjectID*)theID
{
NSError *error = nil;
Trip *theObject = (Trip*)[self.managedObjectContext existingObjectWithID:theID error:&error];
if (error)
NSLog (#"Error retrieving object with ID %#: %#", theID, error);
return theObject;
}

You are not saving your context after applying the changes (new StopOff objects).
After adding a StopOff, call [managedObjectContext save:&error] and you will persist your new object to the store.
CoreData does not save automatically, and without a save: being called, you will loose any temporal changes made to the context.
also, there is no need for [trip addStopObject:newStop]; as CoreData is taking care of that as part of the inverse relationship.

You have a to-many relationship setup: you don't need both newStop.trip = trip and [trip addStopObject:newStop]. I think this is why you are getting your warning.
At some point, you will want to call:
NSError *error = nil;
[managedObjectContext save:&error];

Related

NSManagedObjectContext doesn't update object persistently

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.

Editing multiple objects in CoreData's fetchedObjects

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.

CoreData gets emptied after adding data into it

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.

Saving and Deleting NSManagedObject & NSManagedObjectContext

Three Questions but they are all related. If you like I can divide them into three questions so that you can more credits. Let me know if you'd like for me to do that.
I have the following code that allows me to access NSManagedObject
self.managedObjectContext = [(STAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSError *error;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"LetsMeet" inManagedObjectContext:managedObjectContext]];
NSArray *objectArray = [managedObjectContext executeFetchRequest:request error:&error];
if(objectArray.count==0){
letsMeet = (LetsMeet *) [NSEntityDescription insertNewObjectForEntityForName:#"LetsMeet" inManagedObjectContext:managedObjectContext];
} else{
letsMeet = (LetsMeet *)[objectArray objectAtIndex:0];
}
The code above allows me to save and retrieve attributes. i.e. I can access letsMeet.attribute to save and fetch.
Question 1: How do I delete and start a brand new managedObjectContext. i.e. User has a form that he's been filling out between the scenes. Everything is saved to CoreData from each scene as the user hits the Next button on the navigation Controller. After going through several screens, the user wants to cancel the form. At this point I would like to delete everything that has been saved thus far. Code example please.
Question 2: Lets say the user gets towards to end of the form and decides to save the form for later retrieval. How do I save a copy of the entire form as one object in Core Data. Code example please.
Question 3: How do I retrieve that saved object from Core Data at a later time and display what all the user had saved? Code example please.
To delete you just need to delete letsMeet object from NSManagedObjectContext.
NSError *error;
[managedObjectContext deleteObject:letsMeet];
[managedObjectContext save:&error];
Since you always have only one object, getting the reference of letsMeet is not a problem. You can do as you did in your code.
Update:
And you don't need to delete the managed object context. It just a space to deal with your objects. More explanation at the end of question.
2. If the LetsMeet entity is modeled in a way that all the form elements are attributes of LetsMeet, when you save the managedObjectContext after creating a LetsMeet object as you have done in code, this will be saved as a single object.
3.You already know how to retrieve an object as thats what you are doing in the code. Everything becomes easy as you are only using one object.
In the case of multiple objects to get a the unique object, you should either implement a primary key,(maybe formID, i.e; add another attribute to LetsMeet) or you should know what the objectId of each object is and then set the predicate of your fetch request accordingly.
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:letsMeet];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"formId like %#", formId];
[request setPredicate:predicate];
NSArray *resultsArray =[managedObjectContext executeFetchRequest:request error:&error];
If your formId is unique, this will return you a single object array.
But if you are using core-data for only handling one object, you could've used NSUserDefaults or write to a plist File to do this. This is kind of overkill.
Update:
To get the objectId of a NSManagedObject:
[letsMeet objectId];
ManagedObjectContext is like a whiteboard. The object you have inside the array, the object inside managed object context, its all the same. You can change the objects, add object, delete object etc. Only thing is whatever is the current state of the object(s) when you do a [managedObjectContext save:] , that is written to disk.

Core Data NSValidation error 1620

I'm getting a "failed to save" error, no. 1620, when I try the following:
NSManagedObjectContext *context = [self managedObjectContext];
CustomObject *objToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"CustomObject" inManagedObjectContext:context];
objToInsert.variable1 = [NSNumber numberWithFloat:10.0];
objToInsert.variable2 = [NSDate date];
objToInsert.variable3 = [NSNumber numberWithInt:1];
NSError *error;
if (![context save:&error]) {
NSLog(#"failed to save with error = %#", [error localizedDescription]);
}
This same logic works just fine when saving other NSManagedObject-subclass objects to the managedObjectContext. The 1620 error is listed as being a "number too small" validation error, but there's clearly nothing wrong with the numbers I'm inputting. The three variables are defined in my data model as Float, Date and Integer 16, though I'm not sure whether that's
relevant.
The Core Data stack is all present and correct and as I said, works just fine with other insertion logic. Am I missing something here?
EDIT: turns out from the error output that Core Data is trying to save a completely different NSManagedObject subclass, which I work with earlier in execution but not in this method call. Why would this be?
In the end, the solution was partly as suggested by Martin R (see comments) but for a completely different entity. It appears that if a required minimum value is set for any other entity in the data model, and that entity is modified and saved at some prior point, any subsequent saves to other entities will cause an error. This does not seem to be sensible - maybe I'm doing something wrong elsewhere? - but removing the minimum value requirement did solve this particular problem.

Resources