25

I am trying to observe individual NSManagedObject changes on NSManagedObjectContextWillSaveNotification:

- (void)managedObjectContextWillSave:(NSNotification *)notification
{
    for (NSManagedObject * object in self.mutableObservedManagedObjects)
    {
        if (object.hasChanges)
        {
            [self managedObjectWasUpdated:object];
        }
    }
}

The problem is that hasChanges is true while object.changedValues is empty, thus wrongly (?) triggering managedObjectWasUpdated:.

I'm trying to understand why this is the case and if I should better check object.changedValues.count before calling managedObjectWasUpdated:.


isInserted and isDeleted are both false.

Rivera
  • 10,792
  • 3
  • 58
  • 102

6 Answers6

13

In my experience, if the entity already existed, you loaded it and then you set a value to a property that is equal to its previous value, then the record will be marked as updated, hasChanges will return YES, and changedValues will be empty. When you save the context, what gets updated is a special Core Data column called Z_OPT, which refers to the number of times an entity has been updated. For these situations you can do something like this before saving:

for (NSManagedObject *managedObject in context.updatedObjects.objectEnumerator) {
    if (!managedObject.changedValues.count) {
        [context refreshObject:managedObject mergeChanges:NO];
    }
}

in order to don't even update the Z_OPT value.

  • 2
    I believe this is the most accurate answer. `hasChanges` is based on _everything_ that Core Data knows about (including Z_OPT). `changedValues()` is reporting fields of your entity that are different. In most cases applications probably care more about `changedValues().isEmpty`. – Colin M Aug 29 '16 at 17:24
  • 2
    Instead of checking `changedValues.count` you can use a new property introduced on `NSManagedObject`: `var hasPersistentChangedValues: Bool` – Wojciech Nagrodzki Apr 12 '17 at 13:45
  • 1
    Unfortunately `[NSManagedObjectContext -hasChanges]` still going to be `true` – ReDetection Feb 09 '21 at 14:48
  • Apparently changing a transient value also marks the object as being updated but with no changed values. – Frizlab Jan 22 '23 at 15:13
8

I encountered the same issue. Instead of getting the flags, I just checked if changedValues() is empty.

For Swift:

if !managedObject.changedValues().isEmpty {
    // Has some changed values
}
chrisamanse
  • 4,289
  • 1
  • 25
  • 32
  • That works, but still, this isn't documented behaviour and makes one wonder when Apple will break it... :( – Jan Nash May 10 '16 at 16:20
7

From iOS 7 you can also use hasPersistentChangedValues instead of changedValues. I think this performs better.

Radu Vlad
  • 1,474
  • 2
  • 21
  • 37
  • 3
    Indeed `hasPersistentChangedValues` is the best choice. Based on header file's comment, `hasPersistentChangedValues` returns YES if any persistent properties do not compare `isEqual` to their last saved state. Relationship faults will not be unnecessarily fired. This differs from the existing `-hasChanges` method which is a simple dirty flag and also includes transient properties. – Jonny Nov 02 '18 at 06:59
3

According to doc, hasChanges will return YES if the receiver has been inserted, has been deleted, or has unsaved changes, otherwise NO.

In your case, you can check isInserted, isUpdated, isDeleted flag to find what happened to your managed object. changedValues only show the properties that have been changed since last fetching or saving the receiver.

Ken Kuan
  • 809
  • 6
  • 8
  • 4
    I had followed the documentation and `isInserted`/`isDeleted` were both false, `isUpdated` true, yet no `changedValues`. – Rivera Oct 14 '14 at 04:55
0

Do you have any transient attributes on your entity? I am seeing the behavior you describe, and I've written some test code that shows that modifying a transient attribute causes hasChanges to return true, while changedValues is empty.

You can avoid this behavior by using setPrimitiveValue:forKey: to modify your transient attribute or the equivalent method generated by Core Data (setPrimitiveFoo: for an attribute named foo). You could also implement the transient attribute's setter to do this for you.

Steve Madsen
  • 13,465
  • 4
  • 49
  • 67
0

When another managed object in relationship to an NSManagedObject has changes, in my observation an NSManagedObject will sometimes, but not always, have an isUpdated of true, while changedValues is empty.

On other occasions, isUpdated is false, and again changedValues is empty.

changedValues being empty seems the correct behavior here. I am unclear what is cause of the variability in isUpdated being true. Having observed this in my production code, I am working on a simple sample project with hope of reproducing this for a bug report to Apple if I can.

To solve my functional problem in my case, I modified my code to automatically set a timestampModified on the parent object in cases where I wanted to ensure that isUpdated was always consistently set to true when these objects in relationship had changed.

Duncan Babbage
  • 19,972
  • 4
  • 56
  • 93