Core Data: confused with relationships - ios

I have the following simple relationship:
Parent has many Children (ordered)
Child belongs to Parent.
And I get a strange behaviour:
// In of my classes, I keep a reference to a child.
#interface Foo ()
{
Child *_child;
}
// Somewhere in my code I create a child and a parent and associate them.
Child *c = (Child *)[NSEntityDescription insertNewObjectForEntityForName:#"Child" inManagedObjectContext:moc];
Parent *p = (Parent *)[NSEntityDescription insertNewObjectForEntityForName:#"Parent" inManagedObjectContext:moc];
[p addChildrenObject:c];
c.parent = p;
_child = c;
// Then somewhere else I do:
Parent *parent = _child.parent; // It works fine
NSOrderedSet *children = parent.children; // Same, I do see the children
int idx = [children indexOfObject:_child]; // idx is NSNotFound!!
What I can see is children contains childs with normal IDs, whereas my _child reference still has a temporary ID.
I am using the same context everywhere.
I guess I am doing something wrong with my references, but I am not sure what it is?

In response to your last comment:
I believe it happens when the child ID is updated from temporary to
permanent. Somehow Core Data does something that breaks the reference.
You are almost certainly correct. Here is an exert from the documentation about objectId:
Important: If the receiver has not yet been saved, the object ID is a
temporary value that will change when the object is saved.
That being said, if you have some other unique property on the child you could do use KVO to run comparisons:
int idx = [[children valueForKey:#"uniqueProperty"] indexOfObject:_child.uniqueProperty];

Related

Creating a memory-only NSManagedObject model

I need to create an instance of a NSManagedObject that will not be saved in CoreData and only in memory.
e.g.:
I have the Item and Log NSManagedObject, and they have relations between them.
I want to be able to create a Log instance without any core data properties, and assign its item property to an Item instance.
I know I can create it in a different, memory-persistence, context (or nil context). But then I can't assign the item property, since my Item instance is in the core-data context.
NSEntityDescription *description = [NSEntityDescription entityForName:#"Log" inManagedObjectContext:defaultContext];
Log *log = [[Log alloc] initWithEntity:description insertIntoManagedObjectContext:nil];
log.item = item;
This code throws an exception when ran:
Illegal attempt to establish a relationship 'item' between objects in different contexts
How can I achieve this in another way?
You can create a NSManagedObjectContext with parent context set to your Log's MOC.
Do you need the relationship to have an inverse? If not, you could use the ObjectID for the item objects as an attribute in the Log entity. You would need to convert the ObjectID to its URIRepresentation, and then convert that to a NSString:
NSURL *itemURI = [item.objectID URIRepresentation];
NSString *itemURIstring = [itemURI absoluteString];
log.itemURI = itemURIstring;
(If item has not yet been saved to the database, it will have a temporary ID - you should test for this with item.objectID.isTemporary otherwise the objectID may change). When you want to find the Item object for a given Log object, reverse the process:
NSURL *itemURI = [NSURL URLwithString:log.itemURI];
NSManagedObjectID *itemObjectID = [self.context.persistentStoreCoordinator managedObjectIDForURIRepresentation:itemURI];
Item *item = [self.context objectWithID:itemObjectID];
Pretty cumbersome!
I guess if you need an inverse, you could do the same (i.e. store a URI for the Log object as a string in Item).

define property of EntityB based on those of EntityA

I have a UILabel that displays a managed object of EntityA. I want use the text of that UILabel as a definition for an EntityB managed object. My first question is, is this possible? I'm trying to pull the text and establish it's properties as those of EntityB here:
NSString *temp = managedObjEntityA.nameA;
managedObjEntityA.name = self.UILabel.text;
self.UILabel.text = temp;
EntityB *textEntityB;
temp = textEntityB.nameB;
My hope is to use the defined textEntityB as reference for a newly created object to establish relationship with:
createdObject.objectToB = textEntityB;
Every version I've tried I get nul for textEntityB. How would I call the managed object of EntityB that matches that of EntityA?
You first have to insert EntityB into your managed object context.
EntityB *newB = [NSEntityDescription insertNewObjectForEntityForName:#"EntityB"
inManagedObjectContext:context];
newB.name = aObject.name;
// or
newB.name = self.label.text;
// establish the (to-one) relationship
aObject.objectToB = newB;

Find object by name in NSMutableArray

I have a generic person object with properties personName, lastName, and age. I am storing the user input into an NSMutableArray and I wanted to find a under by his/her name in the array. I have tried finding a bunch of different solutions but none that quite really work.
This is my main.m
#autoreleasepool {
char answer;
char locatePerson[40];
//Create mutable array to add users for retrieval later
NSMutableArray *people = [[NSMutableArray alloc] init];
do{
Person *newPerson = [[Person alloc]init];
[newPerson enterInfo];
[newPerson printInfo];
[people addObject:newPerson];
NSLog(#"Would you like to enter another name?");
scanf("\n%c", &answer);
}while (answer == 'y');
NSLog(#"Are you looking for a specific person?");
scanf("%c", locatePerson);
//This is where I need help
int idx = [people indexOfObject:]
}
This is very basic but I am new to objective-c and I wanted to try and find the user by name. The solutions I've seen have used the indexesOfObjectsPassingTest method. But I was wondering if I can't just use the indexOfObjectmethod the way I did there to locate a person by its name?
Any help is appreciated.
This is one of those hard problems you should avoid with some up-front design. If you know that you are putting things into a collection class and will need to get them out again based on some attribute (rather than by order of insertion) a dictionary is the most efficient collection class.
You can use a NSDictionary keyed with Person's name attribute. You can still iterate over all the objects but you will avoid having to search the whole collection. It can take a surprisingly long time to find a matching attribute in a NSArray! You wouldn't even have to change your Person object, just do
NSDictionary *peopleDictionary = #{ person1.name : person1, person2.name : person2 };
or add them one by one as they are created into a NSMutableArray.
You can try something like this assuming that 'name' is a property for your Person class.
NSUInteger i = 0;
for(Person *person in people) {
if([person.name isEqualToString:locatePerson]) {
break;
}
i++;
}

iOS CoreData+MoGenerator: How do I initialize a Managed Object once only when I am using nested contexts?

I am using mogenerator to generate code from a model with a TestPerson managed object. TestPerson inherits from the abstract object TLSyncParent. In TLSyncParent I have the code:
- (void) awakeFromInsert
{
[super awakeFromInsert];
QNSLOG(#"%#\n%#", self.managedObjectContext, self.description);
if (self.syncStatus == nil) {
self.syncStatusValue = SYNCSTATUS_NEW;
self.tempObjectPID = [self generateUUID];
QNSLOG(#"After init values\n%#", self.description);
}
}
I create the TestPerson object in childMOC whose parent is mainMOC, whose parent is rootMOC. awakeFromInsert runs as expected and makes the init changes. When I save childMOC to mainMOC, awakeFromInsert is run again. From the docs I would not expect that, but there is some ambiguity. From the Docs, "You typically use this method to initialize special default property values. This method is invoked only once in the object's lifetime." The real problem is that when awakeFromInsert runs in mainMOC, the init changes made in childMOC are NOT there. awakeFromInsert is apparently run before the save actually takes place.
2013-10-02 11:22:45.510_xctest[21631:303] TestPerson -awakeFromInsert <NSManagedObjectContext: 0xd684780>
<TestPerson: 0xd6863b0> (entity: TestPerson; id: 0xd684ed0 <x-coredata:///TestPerson/t02B71E0D-AE3F-4605-8AC7-638AE072F2302> ; data: {
dept = nil;
job = nil;
objectPID = nil;
personName = nil;
syncStatus = 0;
tempObjectPID = nil;
updatedAt = nil;
})
2013-10-02 11:22:45.511_xctest[21631:303] TestPerson -awakeFromInsert After init values
<TestPerson: 0xd6863b0> (entity: TestPerson; id: 0xd684ed0 <x-coredata:///TestPerson/t02B71E0D-AE3F-4605-8AC7-638AE072F2302> ; data: {
dept = nil;
job = nil;
objectPID = nil;
personName = nil;
syncStatus = 4;
tempObjectPID = "7AB46623-C597-4167-B189-E3AAD24954DE";
updatedAt = nil;
})
2013-10-02 11:22:45.511_xctest[21631:303] CoreDataController -saveChildContext: Saving Child MOC
2013-10-02 11:22:45.511_xctest[21631:303] TestPerson -awakeFromInsert <NSManagedObjectContext: 0xd682180>
<TestPerson: 0xd68fce0> (entity: TestPerson; id: 0xd684ed0 <x-coredata:///TestPerson/t02B71E0D-AE3F-4605-8AC7-638AE072F2302> ; data: {
dept = nil;
job = nil;
objectPID = nil;
personName = nil;
syncStatus = 0;
tempObjectPID = nil;
updatedAt = nil;
})
2013-10-02 11:22:45.511_xctest[21631:303] TestPerson -awakeFromInsert After init values
<TestPerson: 0xd68fce0> (entity: TestPerson; id: 0xd684ed0 <x-coredata:///TestPerson/t02B71E0D-AE3F-4605-8AC7-638AE072F2302> ; data: {
dept = nil;
job = nil;
objectPID = nil;
personName = nil;
syncStatus = 4;
tempObjectPID = "B799AFDA-3514-445F-BB6F-E4FE836C4F9D";
updatedAt = nil;
})
What is the proper place to initialize a managed object when using the MoGenerator structure?
The documentation on awakeFromInsert is somewhat outdated and doesn't reflect the reality of nested contexts. When it says that the method is
Invoked automatically by the Core Data framework when the receiver is first inserted into a managed object context.
It should really say something like "..first inserted into any managed object context", since (as you've discovered) this happens more than once with nested contexts. Really, the notion of awakeFromInsert is kind of outdated when using nested contexts. The method was clearly designed in the old non-nested days and hasn't adapted.
There are a couple of ways to deal with this. One is a simple run time check, where you do something like:
if ([[self managedObjectContext] parentContext] != nil) {
// Set default values here
}
This code only runs when the current context is a child of some other context. The method still runs for the parent context, but you skip your default value setters. That's fine if you only ever nest one level deep, i.e. one parent with one or more child contexts, but no "grandchild" contexts of the parent. If you ever add another nesting level, you're right back where you started from.
The other option (and the one I usually prefer) is to move the default value code into a separate method, and then not use awakeFromInsert at all. That is, create a method called something like setDefaultValues, which in your case sets the values for syncStatusValue and tempObjectPID. Call this method right after you first create a new instance and nowhere else. Since it never gets an automatic call, the code never runs except when you tell it to run.
I am pretty sure Mogenerator doesn't change the way you create managed objects, but only moves the actual managed object classes to the machine generated files with the "_" prefix and creates subclasses of those managed objects to put all your custom logic in so that it doesn't get lost when you regenerated your managed object classes.
OK, thanks to Tom Herrington, I found a very nice way to do this. It seems to do exactly what I want with a minimum of trouble. It fits perfectly with the MoGenerator structure. I already had a category on NSManagedObject with the method initWithMOC. I added a call to the method awakeFromCreate and provided a default implementation. You just override awakeFromCreate in the same way you would override awakeFromInsert. The only requirement is that you ALWAYS create the MO using the initWithMOC method.
#implementation NSManagedObject (CoreDataController)
+ (NSManagedObject*) initWithMOC: (NSManagedObjectContext*) context
{
NSManagedObject* mo = (NSManagedObject*)
[NSEntityDescription insertNewObjectForEntityForName: NSStringFromClass(self)
inManagedObjectContext: context];
[mo awakeFromCreate];
return mo;
}
- (void) awakeFromCreate
{
return;
}

How to check to see if an object exists in NSMutableArray without knowing the index, and replace it if it exists, in iOS?

I have an NSMutableArray that contains objects of type Person. The Person object contains parameters of NSString *name, NSString *dateStamp, and NSString *testScore. What I would like to do using fast enumeration, is to check to see in the NSMutableArray *testResults, is see if an object with the same name parameter exists.
If it does, then I want to replace the existing object in the NSMutableArray, with the object that I am about to insert which will have the most current dateStamp, and testScore values. If an object with no matching name parameter is found, then simply insert the object that I have.
My code so far looks like this:
This is the code that creates my object that I am about to insert:
Person *newPerson = [[Person alloc] init];
[newPerson setPersonName:newName]; //variables newName, pass, and newDate have already been
[newPerson setScore:pass]; //created and initialized
[newPerson setDateStamp:newDate];
and here is the code where I try to iterate through the NSMutableArray to see if an object with the same name parameter already exists:
for (Person *checkPerson in personList) { //personList is of type NSMutableArray
if (newPerson.newName == checkPerson.name) {
//here is where I need to insert the code that replaces checkPerson with newPerson after a match has been found
}
else {
personList.addObject(newPerson); //this is the code that adds the new object to the NSMutableArray when no match was found.
}
}
It's not a very complicated problem, but I am confused as to how to go about finding a match, and then replacing the actual object without knowing ahead of time what index the object resides in.
You want to use indexOfObjectPassingTest to look for a match:
NSInteger indx = [personList indexOfObjectPassingTest:^BOOL(Person *obj, NSUInteger idx, BOOL *stop) {
return [obj.name isEqualToString:newPerson.name];
}];
if (indx != NSNotFound) {
[personList replaceObjectAtIndex:indx withObject:newPerson];
}else{
[personList addObject:newPerson];
}
Notice that I used isEqualToString: to compare the two strings not ==. That mistake has been asked about and answered a million times on this forum.
You have some inconsistency in your naming. In the question, you say the Person objects have a name property, but when you create a new person you use setPersonName, which would imply that the property name is personName. I assumed, just name in my answer.

Resources