71

I need to write a custom setter method for a field (we'll call it foo) in my subclass of NSManagedObject. foo is defined in the data model and Xcode has autogenerated @property and @dynamic fields in the .h and .m files respectively.

If I write my setter like this:

- (void)setFoo: (NSObject *)inFoo {
    [super setFoo: inFoo];
    [self updateStuff];
}

then I get a compiler warning on the call to super.

Alternatively, if I do this:

- (void)setFoo: (NSObject *)inFoo {
    [super setValue: inFoo forKey: inFoo];
    [self updateStuff];
}

then I end up in an infinite loop.

So what's the correct approach to write a custom setter for a subclass of NSManagedObject?

Andrew Ebling
  • 10,175
  • 10
  • 58
  • 75

6 Answers6

107

According to the documentation, it'd be:

- (void) setFoo:(NSObject *)inFoo {
  [self willChangeValueForKey:@"foo"];
  [self setPrimitiveValue:inFoo forKey:@"foo"];
  [self didChangeValueForKey:@"foo"];
}

This is, of course, ignoring the fact that NSManagedObjects only want NSNumbers, NSDates, NSDatas, and NSStrings as attributes.

However, this might not be the best approach. Since you want something to happen when the value of your foo property changes, why not just observe it with Key Value Observing? In this case, it sounds like "KVO's the way to go".

Community
  • 1
  • 1
Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • Thanks Dave. Apologies the field is actually defined as an `NSNumber *` but I was trying to generalise the problem. I tried what you suggested above, but I get a compiler warning that my class may not respond to `-setPrimitivePositionX:`. Any ideas? Good idea re. KVO. Where would be the best place to register? In `- (void)awakeFromInsert`? I'd de-register in `- (void)dealloc` right? – Andrew Ebling Jun 04 '10 at 06:24
  • OK, I added a private `@interface` section in the .m file and that fixed the warning, but the codes still not behaving as expected. I need to debug this! – Andrew Ebling Jun 04 '10 at 06:28
  • On further investigation the setter is getting called correctly when I explicitly set the value on the object, but it doesn't get called when I use the NSUndoManager to revert the change. In which case I'm guessing KVO is a better all-round approach. – Andrew Ebling Jun 04 '10 at 06:32
  • 1
    If you make the property transient in you Core Data Model, the values get reverted automatically. If you need additional custom processing in undo/redo KVO is the only way to go. If you want to be 10.5 compliant, you need to override - (void)_undoDeletions:(id)deletions of NSManagedObjectContext like in http://qr.cx/iZq – Martin Brugger Jun 04 '10 at 06:34
  • Instead of `setPrimitiveFoo:` you can do `[super setPrimitiveValue:inFoo forKey:@"foo"];` I agree KVO should be better, but it seems complicated to properly register/deregister KVO in a managed object, and I'm worried about performance in my case (hundreds of thousands of objects allocated/deallocated without `foo` changing). – Abhi Beckert Jan 20 '13 at 10:13
  • don't forget to add `willChangeValueForKey` and `didChangeValueForKey` or this wont work, worked for me, thanks. – Fantini Jun 24 '15 at 15:02
19

Here's how I'm doing KVO on the id attribute of a Photo : NSManagedObject. If the photo's ID changes, then download the new photo.

#pragma mark NSManagedObject

- (void)awakeFromInsert {
    [self observePhotoId];
}

- (void)awakeFromFetch {
    [self observePhotoId];
}

- (void)observePhotoId {
    [self addObserver:self forKeyPath:@"id"
              options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"id"]) {
        NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];        
        if (![newValue isEqualToString:oldValue]) {
            [self handleIdChange];
        }
    }
}

- (void)willTurnIntoFault {
    [self removeObserver:self forKeyPath:@"id"];
}

#pragma mark Photo

- (void)handleIdChange {
    // Implemented by subclasses, but defined here to hide warnings.
    // [self download]; // example implementation
}
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • If an object gets deleted, the context saved(object actually deallocated), undo invoked, the observing will be missing. In 10.6+ you can also establish the observing in awakeFromSnapshotEvents. For backwards compatibility take a look at https://github.com/mbrugger/CoreDataDependentProperties It solves exactly all these problems. – Martin Brugger Apr 26 '11 at 17:02
  • 3
    From apple's docs, you should call super on "awakeFromFetch" and "awakeFromInsert" – Fervus Jan 21 '14 at 08:58
  • The [newValue isEqualToString:oldValue] test is unnecessary since the notification will only fire if they are not the same. – Elise van Looij Jun 12 '17 at 13:16
17

I think there is a slight mistake: use

 [self setPrimitiveValue:inFoo forKey:@"foo"];

instead of

 [self setPrimitiveFoo:inFoo];

this works for me.

Martin Brugger
  • 3,168
  • 26
  • 20
  • Thanks Martin. As you say, KVO is the way to go (I'm registering in `-(void)awakeFromFetch` and unregistering in `-(void)dealloc` and I've now implemented this and it works with undo. – Andrew Ebling Jun 04 '10 at 06:54
  • 7
    do not use -(void) dealloc to unregister, unregister observings in -(void) willTurnIntoFault instead. Otherwise you will get unnecessary notifications when an object ist turned into a fault. New objects inserted do not get a -(void) awakeFromFetch message. use -(void) awakeFromInsert too. – Martin Brugger Jun 04 '10 at 07:14
  • 1
    @Andrew Ebling, please answer your own question and include the source code of your solution. (Feel free to change variable names, etc., but please keep it good.) I'm working on doing this exact thing. I'm figuring it out by reading the link on KVC, but seeing your solution would be very helpful! :) – ma11hew28 Apr 26 '11 at 04:11
12

Here is the Apple way for overriding NSManagedObject properties (without breaking KVO), in your .m file:

@interface Transaction (DynamicAccessors)
- (void)managedObjectOriginal_setDate:(NSDate *)date;
@end

@implementation Transaction
@dynamic date;

- (void)setDate:(NSDate *)date
{
    // invoke the dynamic implementation of setDate (calls the willChange/didChange for you)
    [self managedObjectOriginal_setDate:(NSString *)date;

    // your custom code
}

managedObjectOriginal_propertyName is a built-in magic method you just have to add the definition for. As seen at bottom of this page What's New in Core Data in macOS 10.12, iOS 10.0, tvOS 10.0, and watchOS 3.0

malhal
  • 26,330
  • 7
  • 115
  • 133
1

There's a really handy Xcode Snippets menu (Xcode 12 has a + button in the top right) that has great snippets for overriding lots of common Core Data code, including KVO-compliant accessors for object-types (getters + setters).

Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
0

Here's how you do it 1-n (and I presume n-m) relationships:

Lets assume the relationship name is called "students" in an object called "School".

First you need to define the primitive accessor methods for the NSMutableSet. Xcode will not automatically generate these for you.

@interface School(PrimitiveAccessors)
- (NSMutableSet *)primitiveStudents;
@end

Next you can define your accessor method. Here I'm going to override the setter.

- (void)addStudentsObject:(Student *)student
{
  NSSet *changedObjects = [[NSSet alloc] initWithObjects:&student count:1];

  [self willChangeValueForKey:@"students"
              withSetMutation:NSKeyValueUnionSetMutation
                 usingObjects:changedObjects];

  [[self primitiveStudents] addObject:value];

  [self didChangeValueForKey:@"students"
             withSetMutation:NSKeyValueUnionSetMutation
                usingObjects:changedObjects];
}
David
  • 2,429
  • 24
  • 15