Search code examples
iosmacoscocoacore-datansmanagedobjectcontext

Saving parent context ignores changes to encoded value of transient attribute saved to child context


A Core Data text asset has got a transient attribute textStorage which is getting persistently saved in contents of type NSData. In some cases saving the context after editing textStorage Core Data doesn't save the changes. The app is using BSManagedDocument with parent managed object context to save in the background. Below is a pseudo log to illustrate what's happening. The second log is expected to be persistently saved however the parent context reverts back to the original.

Is this down to the way I've chosen for handling the encoding/decoding in the TextAsset implementation (see below)?

Text Storage Logs

Original text storage before editing:

Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
THIS GETS DELETED
Donec ullamcorper nulla non metus auctor fringilla.

Text storage in willSave: in the child (main document) context (after edited in text view):

Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
Donec ullamcorper nulla non metus auctor fringilla.

Text storage in willSave: in the parent (backround saving) context (after child context got saved):

Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.
THIS GETS DELETED
Donec ullamcorper nulla non metus auctor fringilla.

As you can see the 'THIS GETS DELETED' line is still in there.

Relevant parts of the TextAsset.m implementation

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key 
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"contents"]) {
        return [keyPaths setByAddingObject:@"textStorage"];
    }

    return keyPaths;
}

- (void)setTextStorage:(NSTextStorage *)textStorage {
    [self willChangeValueForKey:@"textStorage"];
    [self setPrimitiveValue:textStorage forKey:@"textStorage"];
    [self didChangeValueForKey:@"textStorage"];
}

- (NSTextStorage *)textStorage 
{    
    [self willAccessValueForKey:@"textStorage"];
    NSTextStorage *textStorage = [self primitiveValueForKey:@"textStorage"];
    [self didAccessValueForKey:@"textStorage"];

    if (textStorage == nil) {
        NSData *contents = [self contents];
        if (contents != nil) {
            textStorage = [NSKeyedUnarchiver unarchiveObjectWithData:contents];
            [self setPrimitiveValue:textStorage forKey:@"textStorage"];
        }
    }
    return textStorage;
}

- (void)willSave 
{
    NSTextStorage *textStorage = self.textStorage;

    if (textStorage != nil) {
        [self setPrimitiveContents:[NSKeyedArchiver archivedDataWithRootObject:textStorage]];
    }
    else {
        [self setPrimitiveContents:nil];
    }

    [super willSave];
}

Solution

  • After looking into it further it turns out that the issue gets caused when the contents value of the object in the child context gets pushed to the parent up on save. If the managed object in the parent context is still in memory it will itself still have a the transient textStorage in memory. Hence with the code above it will just archive the textStorage again rather than using the updated contents.

    By resetting the textStorage when (primitive) contents changes this behaviour can be avoided.