Get NSEntityDescription from to-many relationship - ios

I have a user entity with a to-many relationship called "recordings".
The generated interface looks like this:
#interface User : NSManagedObject
#property (nonatomic, retain) NSString * display_name;
// ...
#property (nonatomic, retain) NSSet *recordings; // <-- this one
#end
#interface User (CoreDataGeneratedAccessors)
- (void)addRecordingsObject:(Recording *)value;
- (void)removeRecordingsObject:(Recording *)value;
- (void)addRecordings:(NSSet *)values;
- (void)removeRecordings:(NSSet *)values;
#end
I'm keeping this user inside a session object (singleton) which is used throughout the application. To add a recording, I've created a convenience method that will return an empty "Recording" object which can then be populated by the calling code:
- (Recording *)newRecording
{
Recording *recording = [NSEntityDescription
insertNewObjectForEntityForName:#"Recording"
inManagedObjectContext:self.managedObjectContext];
return recording;
}
I pass the #"Recording" entity name to insertNewObjectForEntityForName:: and this works fine; however, I don't like that my object knows the actual entity name for its .recordings relationship property.
Is there an easy way in which I can say "Create a new object for this relationship"?

You could look at mogenerator and the convenience methods it adds which allow you to ask an NSManagedObject subclass for its entity name. This would be the minimal change to your current code.
Otherwise, you'd be using the entity description to get the relationshipsByName, using the associated relationship description to get the destinationEntity and then you have the name that you need. You could add this as a category method so it's easily accessible.

If you don't want to use mogenerator, as #Wain suggested, you can mirror its approach. Write a class method on Recording to create a new Recording that is related to a particular User.
In Recording.h:
+ (Recording *)insertInManagedObjectContext:(NSManagedObjectContext *)moc;
+ (Recording *)insertWithUser:(User *)user;
In Recording.m:
+ (Recording *)insertInManagedObjectContext:(NSManagedObjectContext *)moc
{
Recording *recording = [NSEntityDescription
insertNewObjectForEntityForName:#"Recording"
inManagedObjectContext:moc];
return recording;
}
+ (Recording *)insertWithUser:(User *)user
{
// an NSManagedObject subclass knows its MOC
Recording *recording = [self insertInManagedObjectContext:user.managedObjectContext];
[recording.users addRecordingsObject:recording];
return recording;
}
You'll also want to write a method to search for an existing Recording object using whatever uniquing criteria are appropriate (the "find-or-create" pattern), and invoke that method within +insertWithUser: instead of always inserting a new Recording.

Related

Handling undefined keys in Core Data

Is it possible to set a handler for undefined keys in Core Data?
I'm asking because despite defining valueForUndefinedKey: my implementation of that method is never called if valueForKey: is invoked on a managed object that doesn't have an attribute with that key.
This is needed for a synchronization system I'm currently writing where an object can be marked as locallyCreated or locallyDeleted but at the same time not all objects are editable so I want to avoid defining these properties for all entities in my model (around ~25 entities).
Although it seems tempting to create a single parent entity for that purpose I would like to avoid doing that since that will put all objects in one giant SQLite table which as far as I know will have negative impact on performance.
Currently I have a base "entity" class called RemoteObject that defines some common attributes like remoteID, locallyCreated, locallyDeleted, as suggested in another answer on SO, which all other entities inherit in code like this:
#interface RemoteObject : NSManagedObject
#property (nonatomic) NSString *remoteID;
#property (nonatomic) BOOL locallyCreated;
#property (nonatomic) BOOL locallyDeleted;
#end
#implementation RemoteObject
#dynamic remoteID;
#dynamic locallyCreated;
#dynamic locallyDeleted;
#end
#interface Project : RemoteObject
// custom properties
#end
What I want is to inspect any given RemoteObjet and see if it was locally create or deleted. However, as I said above, not all of the entities have corresponding attributes, so Core Data will throw an exception.
I found a workaround that allows me to avoid those errors - define a class method instead:
- (id)valueForKeyIfExists:(NSString *)key {
if (self.entity.attributesByName[key] != nil) {
return [self valueForKey:key];
}
return nil;
}
+ (BOOL)objectIsLocallyCreated:(RemoteObject *)object {
return [[object valueForKeyIfExists:#"locallyCreated"] boolValue];
}
But I was wondering if it would be possible to refactor this into object properties instead, catching undefined keys with valueForUndefinedKey: like this:
- (id)valueForUndefinedKey:(NSString *)key {
if ([key isEqualToString:LocallyCreatedKey]
|| [key isEqualToString:LocallyDeletedKey]) {
return #(NO);
}
return [super valueForUndefinedKey:key];
}
- (BOOL)locallyDeleted {
return [[self valueForKey:LocallyDeletedKey] boolValue];
}
It would be better to move those 2 flag attributes into a different class and then anything which doesn't have them is a subclass of RemoteObject and anything which does is a subclass of the new class, perhaps TrackedRemoteObject. Then in your algorithm you can class test to determine conformance.

Why NSManagedObject Subclass can't use class_copyMethodList find dynamic generated method?

When I create a NSManagedObject Subclass Employee,it has a property nameaccording the EntityDescription in xcdatamodelfile. And in the .m file, the code modify it using #dynamic like this:
#interface Employee (CoreDataProperties)
#property (nullable, nonatomic, retain) NSString *name;
#end
#implementation Employee (CoreDataProperties)
#dynamic name;
#end
According to the Apple's Document:
Core Data dynamically generates efficient public and primitive get and set attribute accessor methods and relationship accessor methods for properties that are defined in the entity of a managed object’s corresponding managed object model. Therefore, you typically don’t need to write custom accessor methods for modeled properties.
According this, I think the CoreData Framework will create two method named name and setName:in the runtime. So I use such code to verify my thinking.
Employee *object = [NSEntityDescription insertNewObjectForEntityForName:#"Employee" inManagedObjectContext:self.managedObjectContext];
object.name = #"1";
[[self class] showInstanceMethod:[Employee class]];
+ (void)showInstanceMethod:(Class)class {
unsigned int outCount;
//..show InstanceMethodList
Method *methodsList = class_copyMethodList(class, &outCount);
for (int i = 0; i < outCount; i ++) {
SEL sel = method_getName(*methodsList);
NSString *methodName = NSStringFromSelector(sel);
NSLog(#"\nmethodName:%#\n", methodName);
methodsList++;
}
}
I'm sad it didn't log any method name like name or setName:.But I use this code object.name = #"1"; and didn't have any problem.
When they say "dynamically" they really do mean it - the dynamic implementation seems to be provided only as and when the selector is called (directly or via valueForKey:). You can see this happening if you override resolveInstanceMethod: in your Employee class. Call the super implementation and log the selector name and return value. Presumably the method will be listed by class_copyMethodList at this point, though I've never checked.

Core data one-to-many relationships

I've been searching around the internet for days looking for tutorials on how to work with two entities and adding values for the data and linking them. Here is how my app is set up:
I'm making an app that allows the user to create an athlete and within that athlete they can add multiple sporting events. I have two entities: Athletes and Events with a to-one relation from Athletes to Events and a to-many relationship inversely.
The issue I'm having is what code to write to add the name and opponent attribute values within the Events entitiy, all while making sure that that specific name and opponent match up to only one athlete. I've tried using Core Data Accessor Methods, as well as creating new NSManagedObjects for either entitity and adding the values for specific keys.
I've tried to follow the CoreDataRecipes sample code, as well as the common Core Data tutorials on the web. Can anyone help steer me in the right path with some basic method functions or other tutorials that helped you? Thanks.
You need to stop thinking in terms of RDBMS (relational databases) and start thinking in terms of managed object models. CoreData deals with managing objects and their associations. You can associate Athlete objects to Event objects (by the way I suggest using singular vs. plural for entity names i.e. Athlete and Event vs. Athletes and Events and use singular for to-one relationships and plural for to-many relationships). It's a preference thing but I refer to myself as an Athlete (object) not an Athletes (objects). Makes it all more readable and intuitive as well.
Assuming your entities look like this given how you described the relationships:
#interface Athlete : NSManagedObject
#property (nonatomic, retain) NSString * name;
//... a bunch more attributes
#property (nonatomic, retain) Event *event; // use singluar for relationship name too
//...
#end
#interface Event : NSManagedObject
#property (nonatomic, retain) NSString * eventName;
//... a bunch more attributes
#property (nonatomic, retain) NSSet *athletes; // use plural for relationship name
//...
#end
#implementation MyViewController
//... some method
// fetch the athletes (possibly present in table view or other mechanism for selection)
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Athlete"];
NSArray *athletes = [managedObjectContext executeFetchRequest:fetchRequest error:nil];
// select the althletes (primary & opponent - hardcoded for example)
//...
NSArray *selectedAthletes = [NSArray arrayWithObjects: athletes[0], athletes[1], nil];
// create an event
Event *event = [NSEntityDescription
insertNewObjectForEntityForName:#"Event"
inManagedObjectContext:context];
// add the athletes
[event addAthletes:[NSSet setWithArray:selectedAthletes]];
//...
#end
Now you have an event with the 2 athletes. If you wanted to distinguish between the opponent and challenger then you could create 2 to-one relationships (from Event to Athlete) such as opponent and challenger and associate the Event to the Athlete via something like:
#interface Event : NSManagedObject
#property (nonatomic, retain) NSString * eventName;
//... a bunch more attributes
#property (nonatomic, retain) Athlete *opponent; // use singluar for relationship name
#property (nonatomic, retain) Athlete *challenger;
//...
#end
//...
event.opponent = athletes[0];
event.challenger = athletes[1];
//...

core data - how to assign values of an attribute in core data to a variable

Im experimenting with core data and I've been quite successful until now. Im trying to assign the core data values to variables of a class however Im having no luck! My entity is Player, the attributes are Team and Name. I want to assign the values of the attributes to variables of the Athlete class, and then assign these variable to labels in my controller. I only have one player currently in core data. I tried creating an NSManagedObject subclass of the entity (from reading answers on here however no luck).
THis is what I tried;
Athlete * myAthlete = [Athlete alloc];
Player * corePlayer = [Player alloc];
myAthlete.name = corePlayer.name;
NSLog(#"Player name %#",myAthlete.name);
And this is the error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Player name]: unrecognized selector sent to instance 0x832d960'
I apologies in advance because im sure im approaching the wrong way all together - i guess thats a part of learning.
Thanks.
*********EDIT1:**************
here is my NSManagedObject class
// Player.h
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#interface Player : NSManagedObject
#property (nonatomic, retain) NSString * team;
#property (nonatomic, retain) NSString * name;
#end
and the .m
#import "Player.h"
#implementation Player
#dynamic team;
#dynamic name;
#end
When you create an instance of a core data object, you need to store it in a managed object context, so alloc/init won't work.
You can create a new player instance like this:
Player *newPlayer = [NSEntityDescription insertNewObjectForEntityForName:#"Player" inManagedObjectContext:myManagedObjectContext];
The exception you are getting is because there is no name property on the Player entity, that might be related to the initialisation but could be a different problem, you should post details of those classes as well.
Edit:
OK so to get an existing object you need to find it from a managed object context, rather than creating a new one. so given a managed object context which contains one Player, this code will fetch an array which contains one player object:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Player"];
NSArray *playerArray = [managedObjectContext executeFetchRequest:request error:&error];
Then to access properties just get the player object:
Player *player = playerArray[0];
NSLog(#"team: %#", player.team);
NSLog(#"name: %#", player.name);

CoreData transient relationship example

Does anybody have an example on how to model and code a transient to-one relationship in CoreData? For example, I have 2 entities with a one-to-many relationship. Doctor and Appointment. Now I want an transient relationship called mostRecentAppointment on the doctor entity. It's straightforward to model in the xcode designer, but I'm not sure about the implementation side. Also should I implement an inverse? Seems silly.
Have a look at this code I wrote recently, to cache an image in an NSManagedObject:
First you define a transient property in your model (notice that if your transient property points to an object type other than those supported by CoreData you'll leave as "Undefined" in the model)
Then, you re-generate your NSManagedObject subclass for that entity or just add the new property manually, the header file should look like this:
#interface Card : NSManagedObject
#property (nonatomic, retain) NSString * imagePath;
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSNumber * order;
#property (nonatomic, retain) NSString * displayName;
#property (nonatomic, retain) UIImage *displayImage;
#end
Here we change the class of the transient property to the actual class type
e.g. displayImage type here is UIImage.
In the implementation file (or an extension class) you implement the getter/setter for your transient property:
-(UIImage*)displayImage{
//Get Value
[self willAccessValueForKey:#"displayImage"];
UIImage *img = (UIImage*)[self primitiveValueForKey:#"displayImage"];
[self didAccessValueForKey:#"displayImage"];
if (img == nil) {
if ([self imagePath]) { //That is a non-transient property on the object
img = [UIImage imageWithContentsOfFile:self.imagePath];
//Set Value
[self setPrimitiveValue:img forKey:#"displayImage"];
}
}
return img;
}
Hope that helps you.
What you need to do is add an entity of type Appointment called newAppointment and set this each time you create a new appointment for a given doctor. Its that simple.
Always implement an inverse as apple recommend this for validation and core data efficiency.
Alternatively you could timestamp the appointments and use NSPredicates to search for the latest appointment in a given Doctor's linked appointments.
In this case, the appropriate method to override is -awakeFromFetch in the Doctor entity, for example like so:
- (void)awakeFromFetch {
[super awakeFromFetch];// important: call this first!
self.mostRecentAppointment = <something>; // normal relationship
self.mostRecentAppointment.doctor = self; // inverse relationship
}
In the model designer, mark both the normal and the inverse relationship as transient. That should be it.
Well, you'll just have to try out, in your own sample program that can be no more than an hour to set up correctly.
My guess is --- no extra coding will be needed. If Apple's documentation on CoreData is correct, the only difference between a normal attribute/relationship and a "transient" one is that the latter is not persisted, meaning, when you "save" it does not update the persistent-store.
I would guess that otherwise all the aspects of it are complete, together with KVO/KVC compliance, Undo support, validation, and automatic update by delete rules. The only thing is that after a fresh Fetch of the entity --- the transient relationship will always be nil.
For that --- I would of course NOT RECOMMEND setting up a transient relationship as "non-optional", because it is very likely to be null most of the time for most of the entities.
I would set up a reverse relationship (transient as well and named wisely) and have both delete rules be "Nullify".
So far is for transient relation.
But here is an alternative I came up with, trying to solve almost-the-same problem. My "appointment" is one of the related appointments, but not just the "latest", but the first "unfinished" one. Very similar logic.
Instead of a transient relationship, I added a new calculated property to my "Doctor" entitys generated NSManagedObject subclass, in a category, like this:
#interface XXDoctor (XXExtensions)
/**
#brief Needs manual KVO triggering as it is dependent on a collection.
Alternatively, you can observe insertions and deletions of the appointments, and trigger KVO on this propertyOtherwise it can be auto-
#return the latest of the to-many appointments relation.
**/
#property (readonly) XXAppointment *latestAppointment; // defined as the
#end
Implementation:
#import "XXDoctor".h"
#import "XXAppointment.h"
#implementation XXDoctor (XXExtensions)
// this won't work because "appointments" is a to-many relation.
//+ (NSSet *)keyPathsForValuesAffectingLatestAppointment {
// return [NSSet setWithObjects:#"appointments", nil];
//}
- (XXAppointment *) latestAppointment {
NSInteger latestAppointmentIndex = [self.appointments indexOfObjectPassingTest:^BOOL(XXAppointment *appointment, NSUInteger idx, BOOL *stop) {
*stop = (appointment.dateFinished == nil);
return *stop;
}];
return (latestAppointmentIndex == NSNotFound) ? nil : [self.appointments objectAtIndex: latestAppointmentIndex];
}
#end

Resources