22

I have an attribute modificationDate in my Entity A. I want to set its value whenever NSManagedObject is saved. However, if i try to do that in NSManagedObject willSave: method, i get an error:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Failed to process pending changes before save.  The context is still dirty after 100 attempts.  Typically this recursive dirtying is caused by a bad validation method, -willSave, or notification handler.' ***

So, i'm wondering, what's the best way to set the value of modificationDate?

Shmidt
  • 16,436
  • 18
  • 88
  • 136
Mustafa
  • 20,504
  • 42
  • 146
  • 209

5 Answers5

48

In fact the apple docs (which are only half read in the accepted answer) don't recommend this method. They explicitly say you should use NSManagedObjectContextWillSaveNotification. An example might be:

@interface TrackedEntity : NSManagedObject
@property (nonatomic, retain) NSDate* lastModified;
@end

@implementation TrackedEntity
@dynamic lastModified;

+ (void) load {
    @autoreleasepool {
       [[NSNotificationCenter defaultCenter] addObserver: (id)[self class]
                                                selector: @selector(objectContextWillSave:)
                                                    name: NSManagedObjectContextWillSaveNotification
                                                  object: nil];
    }
}

+ (void) objectContextWillSave: (NSNotification*) notification {
   NSManagedObjectContext* context = [notification object];
   NSSet* allModified = [context.insertedObjects setByAddingObjectsFromSet: context.updatedObjects];
   NSPredicate* predicate = [NSPredicate predicateWithFormat: @"self isKindOfClass: %@", [self class]];
   NSSet* modifiable = [allModified filteredSetUsingPredicate: predicate];
   [modifiable makeObjectsPerformSelector: @selector(setLastModified:) withObject: [NSDate date]];
}
@end

I use this (with a few other methods: primary key for example) as an abstract base class for most core data projects.

Paul de Lange
  • 10,613
  • 10
  • 41
  • 56
  • I like! Was just looking for something like this - appreciate the sample code. Do I need to call [load super]? (Suspecting no if NSManagedObject or its parent does nothing with it.) – Joe D'Andrea May 23 '12 at 18:12
  • 1
    OK! I tried this, and it seems to be too overzealous (or more likely I'm misusing it). I have a managed object that uses lastModified (via MYAPPTrackedManagedObject, basically TrackedEntity above). However, this object contains relationships to other objects that do _not_ use lastModified. Thus, setLastModified: will not be recognized by those other objects, and an exception is thrown. Perhaps I need to dial it back a bit somehow? – Joe D'Andrea May 23 '12 at 19:49
  • Ahh, maybe just walk allModified and filter out anything that doesn't respond to setLastModified:, and use that. – Joe D'Andrea May 23 '12 at 19:56
  • I edited the answer to include the filter for you. I don't call [load super] and have no (noticeable) problems, it can't really hurt though...can it? :) – Paul de Lange May 24 '12 at 09:07
  • Why do you do this in `+load` and not `+initialize`? – MJN Oct 27 '13 at 02:00
  • 1
    +load is called when the runtime loads the class - ie: guaranteed to be called before a context update is made. +initialize is called the first time a class is accessed. Just to make sure all notifications are caught, I use +load. – Paul de Lange Oct 28 '13 at 08:07
  • I've been trying to figure it out, but I cant seem to; Why did you use the `@autoreleasepool`? any advice? – Gagan Singh Jun 10 '14 at 21:03
  • 3
    load is called when the class is added to the ObjC runtime. This is usually before the all-enclosing @autoreleasepool that is found in the main.m file. If you don't have an autoreleasepool setup, objects will leak. – Paul de Lange Jun 12 '14 at 02:57
  • so the `NSManagedObjectContextWillSaveNotification` occurs before any object validation? this is something i found unclear from the docs – Sam Sep 25 '14 at 18:06
  • Shouldn't the predicate be `[NSPredicate predicateWithFormat: @"self isKindOfClass: %@", self]`, because here `self` is `TrackedEntity`? – Benjohn Jul 16 '15 at 13:32
33

From the NSManagedObject docs for willSave:

If you want to update a persistent property value, you should typically test for equality of any new value with the existing value before making a change. If you change property values using standard accessor methods, Core Data will observe the resultant change notification and so invoke willSave again before saving the object’s managed object context. If you continue to modify a value in willSave, willSave will continue to be called until your program crashes.

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 maybe something along the lines of:

-(void)willSave {
    NSDate *now = [NSDate date];
    if (self.modificationDate == nil || [now timeIntervalSinceDate:self.modificationDate] > 1.0) {
        self.modificationDate = now;
    }
}

Where you can adjust the 1.0 to reflect the minimum delta between your expected save requests.

Community
  • 1
  • 1
CalloRico
  • 1,158
  • 12
  • 9
9

Actually a much better way than the accepted answer would be to use primitive accessors, as suggested in NSManagedObject's Documentation

`

- (void)willSave
{
    if (![self isDeleted])
    {
        [self setPrimitiveValue:[NSDate date] forKey:@"updatedAt"];
    }
    [super willSave];
}

`

Also, check whether the object is marked for deletion with -isDeleted, as -willSave gets called for those too.

zmit
  • 577
  • 5
  • 9
  • 8
    The docs mention this about overriding willSave: "If you change property values using primitive accessors, you avoid the possibility of infinite recursion, but Core Data will not notice the change you make." – pietrorea May 14 '14 at 13:43
  • @PietroRea is it going to be saved to database? – pronebird Sep 13 '15 at 10:51
8

There are obviously several good solutions to this question already, but I wanted to throw out a new one that worked best for one particular scenario I encountered.

(In Swift:)

override func willSave() {
    if self.changedValues()["modificationDate"] == nil {
        self.modificationDate = NSDate()
    }

    super.willSave()
}

The reason I needed this is because I have the peculiar requirement of needing to sometimes set the modificationDate manually. (The reason I sometimes set the time stamp manually is because I try to keep it in sync with a time stamp on the server.)

This solution:

  1. Prevents the infinite willSave() loop because once the time stamp is set, it will appear in changedValues()
  2. Doesn't require using observation
  3. Allows for setting the time stamp manually
Richard Venable
  • 8,310
  • 3
  • 49
  • 52
  • interesting. we are trying to avoid using any client timestamps for anything critical on the server. the devices clock is untrustworthy maybe cause out-of-order errors in sync algorithms. (it's common to store a 'lastSynced' value on the client and use it to ask the server for only those objects that have changed). – Sam Sep 25 '14 at 18:11
  • @Sam, I'm using this for data in which the user editing it has the *authority* to resolve merge conflicts. Although I don't provide a UI for resolving, so I just use the time stamps to automatically resolve the conflicts. Since the user has the authority over this data, is does not bother me that their clock might be wrong. It may bother my user, but they will probably also be bothered my missing their appointments and such :). But your point is valid, and I don't think I would employ this method for publicly owned data. – Richard Venable Sep 25 '14 at 21:14
0

Swift 4 solution which is a combination of zmit and Richard answer without the need of recurring to NSNotification:

override func willSave() {
    let expectedNewValue = "Your new value"
    if customField != expectedNewValue, changedValues()[#keyPath(Entity.customField)] == nil, !isDeleted {
        customField = expectedNewValue
    }
    super.willSave()
}
matsoftware
  • 766
  • 6
  • 12