Search code examples
ioscore-dataios8nsmanagedobjectkey-value-observing

KVO and Core Data - Self observing managed object


I think this question is quite simple and common, but I still do not understand why it doesn't work. Let me expose the context:

Let's say I have a nice Core Data Model with an entity called Document. This document has a Type, a Date, a Number and a Version... For instance, Type: D, Date: 17-10-2015, Number: 24 and Version 3. This document has and Identifier calculated with those four values: D20151017-24-R03.

There will be a lot of these documents, and I will have to search them by its Identifier, and I also will use a lot of NSFetchedResultsController. So the transient possibility is right out.

Here is what I've done. First register for the observation of the four related attributes:

- (instancetype)initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context {
    self = [super initWithEntity:entity insertIntoManagedObjectContext:context];

    if (self) {
        [self addObserver:self forKeyPath:_Property(documentTypeRaw) options:0 context:KVODocumentIdContext];
        [self addObserver:self forKeyPath:_Property(date) options:0 context:KVODocumentIdContext];
        [self addObserver:self forKeyPath:_Property(number) options:0 context:KVODocumentIdContext];
        [self addObserver:self forKeyPath:_Property(version) options:0 context:KVODocumentIdContext];
    }

    return self;
}

Then, unregister when deallocated:

- (void)dealloc {
    [self removeObserver:self forKeyPath:_Property(documentTypeRaw) context:KVODocumentIdContext];
    [self removeObserver:self forKeyPath:_Property(date) context:KVODocumentIdContext];
    [self removeObserver:self forKeyPath:_Property(number) context:KVODocumentIdContext];
    [self removeObserver:self forKeyPath:_Property(version) context:KVODocumentIdContext];
}

And at last, managed the notifications:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == KVODocumentIdContext) {
        [self updateDocumentId];
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

Just here the updateDocumentId:

- (void) updateDocumentId {
    NSString * prefix = [self documentTypePrefix:self.documentTypeRaw];
    NSString * date = [self.date documentIdFormat];
    NSString * number = [NSString stringWithFormat:@"%.2d",[self.number shortValue]];
    NSString * version = [self.version isEqualToNumber:@0]?@"":[NSString stringWithFormat:@"-R%.2d",[self.version shortValue]];

    self.documentId = [NSString stringWithFormat:@"%@%@-%@%@",prefix,date,number,version];
}

For me this should have worked perfectly... But... It does not...

I have a nice:

failed: caught "NSInternalInconsistencyException", "<MBSDocument: 0x7fd9dbb45f40> (entity: MBSDocument; id: 0x7fd9dbb3cd00 <x-coredata:///MBSDocument/tB55CB581-AEC0-4211-A78A-7C48377BACC2612> ; data:
...
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: date
Observed object: <MBSDocument: 0x7fd9dbb45f40> (entity: MBSDocument; id: 0x7fd9dbb3cd00 <x-coredata:///MBSDocument/tB55CB581-AEC0-4211-A78A-7C48377BACC2612> ; data:
...

I've tried many things, among them removing the call to super in the observeValueForKeyPath:ofObject:change:context:, or registering in the init, etc. But nothing worked. Well, some help would be greatly appreciated.

Thanks in advance.

Edit: This is how the context is defined:

static void * KVODocumentIdContext = &KVODocumentIdContext;

Edit 2: The document class inherits from NSManagedObject.


Solution

  • First thing first: I wouldn't override initWithEntity:

    This is an excerpt from Apple's official API documentation for NSManagedObject class:

    "You are also discouraged from overriding initWithEntity:insertIntoManagedObjectContext:, or dealloc. Changing values in the initWithEntity:insertIntoManagedObjectContext: method will not be noticed by the context and if you are not careful, those changes may not be saved. Most initialization customization should be performed in one of the awake… methods."

    So having given you should probably add these KVO observations in either awakeFromInsert: or awakeFromFetch: (then remove these observers in didTurnIntoFault) overridden methods of your subclass, perhaps you can spare yourself from all this overhead of adding and removing observers depending on what affects your computed property.

    In case the keypaths affecting the computed property aren't to many relationships, then you might as well just write your documentID computed property getter accessor and implement class method +(NSSet *)keYPathsForValiesAffectingDocumentID which returns an NSSet containing the keypaths that if changed will cause the computer property to be recomputed using the new values.