I'm looking for a way to track the attribute change of an NSManagedObject.
Currently I use a NSNotifactionCenter to see the changes of my managedobjectcontext:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleDataModelChange:) name:NSManagedObjectContextObjectsDidChangeNotification object:self.managedObjectContext];
It fires the handleDataModelChange Methode which looks like this:
- (void)handleDataModelChange:(NSNotification *)note
{
NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey];
if (updatedObjects.count > 0) {
for (NSManagedObject *obj in updatedObjects.allObjects) {
NSLog(#"Object updated: %# with values:",obj.entity.name);
NSDictionary *theAttributes = [self getAllAttributesOf:obj];
for (NSString *attributeName in theAttributes) {
NSLog(#"Name: %# : %#",attributeName,[obj valueForKey:attributeName]);
}
}
}
}
This logs the new attributes of the object if it changed. How can I achieve a way to get the old attribute values as well?
From the NSManagedObject Class Reference:
changedValues
Returns a dictionary containing the keys and (new) values of persistent properties that have been changed since last fetching or saving the receiver.
changedValuesForCurrentEvent
Returns a dictionary containing the keys and old values of persistent properties that have changed since the last posting of NSManagedObjectContextObjectsDidChangeNotification.
Related
How can I get the NSManagedObjectID of an object directly after saving?
I've tried using the NSNotification NSManagedObjectContextDidSaveNotification and getting the updated/inserted values and getting the object id from one (the only) managed object, but it's giving me "Unrecognized Selector" when I try to grab the object id.
Can I even get the Object Id right after I save?
- (void)handleDidSaveNotification:(NSNotification *)note
{
NSDictionary *dict = [note userInfo];
NSDictionary *updatedDict = [dict valueForKey:#"updated"];
NSLog(#"Notification: %#", dict);
NSLog(#"Updated Info: %#", updatedDict);
NSManagedObject *core = [updatedDict valueForKey:#"entity"];
NSManagedObjectID *objectId = [core objectID];
}
You are trying to set a dictionary (updatedDict) when the returned data is a NSSet.
you might simply need to get it from the set collection it is in ...
NSSet* s = [dict valueForKey:#"updated"];
[s valueForKey:#"objectID"]
This will return a set of NSManagedObjectIDs.
See NSSet on how to access objects.
I am working on an iOS application that requires Heavy Weight Migration of a single attribute of an entity. The attribute is of type Integer16 in the original Data Model schema, and I need to change the type to a String in the new schema. The relationships that exist in the original model will remain the same in the new model.
This is the first time I'm doing a heavy weight migration, and thankfully its not a very complex one but unfortunately I am very unsure as to how to do this. I have created a MappingModel, and in turn, I've also created a subclass of NSEntityMigrationPolicy which is referenced from the mapping model that I've created. I realize that I need to override the method: createDestinationInstancesForSourceInstance in this subclass, which I am trying to do as follows:
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError *__autoreleasing *)error {
NSLog(#"boo");
NSManagedObjectContext *destMOC = [manager destinationContext];
NSString *destEntityName = [mapping destinationEntityName];
NSString *name = [sInstance valueForKey:#"zip"];
return YES;
}
After running my project, the values for the attribute that already exist in my application come out to (null) which is not surprising, and the above output appears in the console as many times as there are records that need to be migrated over. However, how would I now convert the attribute from Integer16 to String?
Here is a bit of sample code for the createDestinationInstancesForSourceInstance. Basically you loop over the attributes and modify the value of the attribute(s) of interest. Attributes that do not require change are simply copied across.
The actual conversion from the old value (int) to the new value (string) should be placed where is says here do conversion as needed.
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)inSourceInstance
entityMapping:(NSEntityMapping *)inMapping
manager:(NSMigrationManager *)inManager
error:(NSError **)outError {
NSManagedObject *newObject;
NSEntityDescription *sourceInstanceEntity = [inSourceInstance entity];
// correct entity? just to be sure
if ( [[sourceInstanceEntity name] isEqualToString:#"<-the_entity->"] ) {
newObject = [NSEntityDescription insertNewObjectForEntityForName:#"<-the_entity->" inManagedObjectContext:[inManager destinationContext]];
// obtain the attributes
NSDictionary *keyValDict = [inSourceInstance committedValuesForKeys:nil];
NSArray *allKeys = [[[inSourceInstance entity] attributesByName] allKeys];
// loop over the attributes
for (NSString *key in allKeys) {
// Get key and value
id value = [keyValDict objectForKey:key];
if ( [key isEqualToString:#"<-the_attribute->"] ) {
// === here retrieve old value ==
id oldValue = [keyValDict objectForKey:key];
// === here do conversion as needed ==
// === then store new value ==
[newObject setValue:#"<-the_converted_string->" forKey:key];
} else { // no need to modify the value. Copy it across
[newObject setValue:value forKey:key];
}
}
[inManager associateSourceInstance:inSourceInstance withDestinationInstance:newObject forEntityMapping:inMapping];
}
return YES;
}
In your method, you are calling some getters from NSMigrationManager and NSEntityMapping and you store these values in local variables that get discarded once the method finishes.
What exactly did you expect it to do apart from NSLog(#"boo");?
I want to get the inserted and the update objects from NSPersistentStoreDidImportUbiquitousChangesNotification to do some check on them.
Objects can be of two kind of classes: "Alpha" and "Beta". Both classes have the
property (nonatomic, retain) NSString* name
which is the one I should check.
How do I get it?
The following code doesn't work because it says "name" is an unknown selector:
-(void) checkObjects
{
NSDictionary *insertedObjects = [[note userInfo] objectForKey: #"inserted"];
NSDictionary *updatedObjects = [[note userInfo] objectForKey: #"updated"];
for(NSManagedObject *obj in insertedObjects){
if([obj.entity.managedObjectClassName isEqualToString:#"Alpha"]){
Alpha *alpha = (Alpha*) obj;
if (alpha.name isEqualToString:#"xyz"){
//Do some check
}
}else if([obj.entity.managedObjectClassName isEqualToString:#"Beta"]){
Beta *beta = (Beta*) obj;
if (beta.name isEqualToString:#"xyz"){
//Do some check
}
}
}
}
If I change:
Alpha *alpha = (Alpha*) obj;
Beta *beta = (Beta*) obj;
To:
Alpha *alpha = (Alpha*) obj.entity;
Beta *beta = (Beta*) obj.entity;
alpha = Alpha <-- It is the name of the class, not of the object I want!
beta = Beta <--- It is the name of the class, not of the object I want!
When you get NSPersistentStoreDidImportUbiquitousContentChangesNotification, the objects in userInfo are not managed objects, they're managed object IDs. That is, instances of NSManagedObjectID. If you want to look up attributes on the managed object, you need to get the object corresponding to the ID. Something like
NSDictionary *insertedObjectIDs = [[note userInfo] objectForKey:NSInsertedObjectsKey];
for(NSManagedObjectID *objID in insertedObjects) {
NSError *error = nil;
NSManagedObject *obj = [self.managedObjectContext existingObjectWithID:objID error:&error];
....continue...
}
You may need to change that if self doesn't have a managed object context.
Also, on a slight tangent-- it's generally better to use NSInsertedObjectsKey instead of #"inserted" and NSUpdatedObjectsKey instead of #"updated". Apple probably won't change the key names, but they could, so using the key names instead of string literals is a better choice.
I added a lastModifiedDate attribute to all my entities to avoid duplicates when syncing the UIManagedDocument with iCloud, something that I found it can happen if I create new entities with an offline device (iPad) and, at the same time, I create the same entities using another online device (iPhone).
I wanted to set this attribute whenever an object changes so I subscribed for NSManagedObjectContextObjectsDidChangeNotification. The code I wrote to set the lastModifiedDate creates an infinite loop because by setting the lastModificationDate attribute it creates a change that will be notified again by NSManagedObjectContextObjectsDidChangeNotification and so on...
Is it possible to fix it? Is there a better way to accomplish my goal? Should I subclass managedObjectContext and override willSave:?
//At init...
[[NSNotificationCenter defaultCenter] addObserver:applicationDatabase
selector:#selector(objectsDidChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:applicationDatabase.managedDocument.managedObjectContext];
(void) objectsDidChange: (NSNotification*) note
{
// creates an infinite loop
NSDate *dateOfTheLastModification = [NSDate date];
NSMutableArray *userInfoKeys = [[note.userInfo allKeys] mutableCopy];
for(int i=0; i< userInfoKeys.count;i++){
NSString *key = [userInfoKeys objectAtIndex:i];
if([key isEqualToString:#"managedObjectContext"]){
[userInfoKeys removeObject:key];
}
}
for(NSString *key in userInfoKeys){
NSArray *detail = [note.userInfo objectForKey:key];
for (id object in detail){
[object setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
}
To avoid the infinite loop, you could set the last modification date using the
primitive accessor:
[object setPrimitiveValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
because that does not fire another "change" notification. But that also implies that
no observers will see the change.
Overriding willSave in the managed object subclass would suffer from the same problem.
The Apple documentation for willSave states:
For example, if you set a last-modified timestamp, you should check
whether either you previously set it in the same save operation, or
that the existing timestamp is not less than a small delta from the
current time. Typically it’s better to calculate the timestamp once
for all the objects being saved (for example, in response to an
NSManagedObjectContextWillSaveNotification).
So you should register for NSManagedObjectContextWillSaveNotification instead,
and set the timestamp on all updated and inserted objects in the managed object
context. The registered method could look like this:
-(void)contextWillSave:(NSNotification *)notify
{
NSManagedObjectContext *context = [notify object];
NSDate *dateOfTheLastModification = [NSDate date];
for (NSManagedObject *obj in [context insertedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
for (NSManagedObject *obj in [context updatedObjects]) {
[obj setValue:dateOfTheLastModification forKey:#"lastModifiedDate"];
}
}
This assumes that all your entities have a lastModifiedDate attribute, otherwise
you have to check the class of the objects.
//set up notifications
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(dataChanged:)
name:NSManagedObjectContextDidSaveNotification
object:context];
//later
- (void)dataChanged:(NSNotification *)notification{
NSDictionary *info = notification.userInfo;
NSSet *insertedObjects = [info objectForKey:NSInsertedObjectsKey];
NSSet *deletedObjects = [info objectForKey:NSDeletedObjectsKey];
NSSet *updatedObjects = [info objectForKey:NSUpdatedObjectsKey];
Is there anyway to determine from the updatedObjects which fields were actually changed?
thanks,
Michael
The following should do the trick, but you will need to use NSManagedObjectContextWillSaveNotification and access your updated objects through the same NSManagedObjectContext used to save the objects.
for(NSManagedObject *obj in updatedObjects){
NSDictionary *changes = [obj changedValues];
// now process the changes as you need
}
See the discussion in the comments.