Core-Data willSave: method

10,976

Solution 1

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.

Solution 2

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.

Solution 3

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.

Solution 4

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
Share:
10,976

Related videos on Youtube

Mustafa
Author by

Mustafa

I'm a Manager Development/Project Manager/Team Lead/Mobile Application Developer located in Islamabad, Pakistan, working for BroadPeak Technologies. I'm currently focusing on managing and developing mobile applications for Android and iOS devices; with hands on experience developing iOS applications. More information.

Updated on May 14, 2020

Comments

  • Mustafa
    Mustafa over 3 years

    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?

  • Joe D'Andrea
    Joe D'Andrea over 11 years
    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
    Joe D'Andrea over 11 years
    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
    Joe D'Andrea over 11 years
    Ahh, maybe just walk allModified and filter out anything that doesn't respond to setLastModified:, and use that.
  • Paul de Lange
    Paul de Lange over 11 years
    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? :)
  • MJN
    MJN about 10 years
    Why do you do this in +load and not +initialize?
  • Paul de Lange
    Paul de Lange about 10 years
    +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.
  • pietrorea
    pietrorea over 9 years
    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."
  • Gagan Singh
    Gagan Singh over 9 years
    I've been trying to figure it out, but I cant seem to; Why did you use the @autoreleasepool? any advice?
  • Paul de Lange
    Paul de Lange over 9 years
    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.
  • Sam
    Sam about 9 years
    so the NSManagedObjectContextWillSaveNotification occurs before any object validation? this is something i found unclear from the docs
  • Sam
    Sam about 9 years
    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).
  • Richard Venable
    Richard Venable about 9 years
    @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.
  • Benjohn
    Benjohn over 8 years
    Shouldn't the predicate be [NSPredicate predicateWithFormat: @"self isKindOfClass: %@", self], because here self is TrackedEntity?
  • malhal
    malhal about 8 years
    you need to also check if modificationDate is in the changedValues
  • pronebird
    pronebird about 8 years
    @PietroRea is it going to be saved to database?