Search code examples
iphonecore-datansmanagedobjecttransient

Should I have to manually update a transient property even if dependent keys are defined?


I started working with transient properties a little while ago, and thought I understood them pretty well, but this one isn't behaving how I thought it would. It is defined in the data model as a transient property with undefined type, and is declared as a property thus:

@property (nonatomic, readonly) NSSet * manufacturers;

This property is declared to be dependent on another 1:M relationship on this object:

+ (NSSet *) keyPathsForValuesAffectingManufacturers
{
    return [NSSet setWithObject:@"lines"];
}

I have implemented a fairly standard getter for this transient property:

- (NSSet *) manufacturers
{
    [self willAccessValueForKey:@"manufacturers"];
    NSSet *mfrs = [self primitiveManufacturers];
    [self didAccessValueForKey:@"manufacturers"];

    if (mfrs == nil)
    {
        NSMutableSet *working = [NSMutableSet set];

        /* rebuild the set of manufacturers into 'working' here */

        mfrs = working;
        [self setPrimitiveManufacturers:mfrs];
    }

    return mfrs;
}

The part that isn't working is that the cached primitive value for manufacturers doesn't appear to be wiped out when objects are added / removed from the lines relationship on this object. The manufacturers accessor is definitely called when you add / remove lines, but [self primitiveManufacturers] still returns the old cached value (which is obviously non-nil), so the code to rebuild the current set of manufacturers isn't called and I end up with an out-of-date return value.

Am I misunderstanding something about how cached transient property values are supposed to work? Or am I supposed to manually nullify the cached value when lines are changed through some kind of KVO callback so that the update code inside the "if" is executed the next time manufacturers is accessed?

Any help much appreciated.

Edit: I am aware that I'm basically simulating a 1:M relationship with this transient property (i.e. a transient relationship, for want of a better word), and I'm also aware that the documentation warns against using setPrimitiveValue:forKey: for relationships:

You should typically use this method only to modify attributes (usually transient), not relationships. If you try to set a to-many relationship to a new NSMutableSet object, it will (eventually) fail. In the unusual event that you need to modify a relationship using this method, you first get the existing set using primitiveValueForKey: (ensure the method does not return nil), create a mutable copy, and then modify the copy

However, since I am not setting a primitive value on a real 1:M relationship, only my simulated transient one, I had assumed that this warning did not apply to my use case.

Edit #2: Since I am only trying to track when objects are added or removed from the lines set (this is the only way the expected value of manufacturers can change), as opposed to trying to track updates to attributes on the objects that are already in the lines set, I don't think I should need anything as heavyweight as this: https://github.com/mbrugger/CoreDataDependentProperties

Solution: In light of the answer below, I implemented the following solution to blank out the manufacturers set when lines is updated. Can anyone see any issues with this, or suggest any more efficient ways of implementing it? It is important that this operation be fast because it is done in the main thread and will visibly delay UI updates that happen at the same time as the line updates are done.

- (void) awakeFromFetch
{
    [super awakeFromFetch];
    [self addObserver:self forKeyPath:@"lines" options:0 context:nil];
}


- (void) awakeFromInsert
{
    [super awakeFromInsert];
    [self addObserver:self forKeyPath:@"lines" options:0 context:nil];
}


- (void) didTurnIntoFault
{
    [self removeObserver:self forKeyPath:@"lines"];
    [super didTurnIntoFault];
}


- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([object isEqual:self] && [keyPath isEqualToString:@"lines"])
    {
        [self setPrimitiveManufacturer:nil];
    }
    else
    {
        [super observeValueForKeyPath:keyPath 
                             ofObject:object 
                               change:change 
                              context:context];
    }
}

Solution

  • keyPathsForValuesAffectingManufacturers does not mean that the value of manufacturers will be deleted when lines is changed, it means that objects observing manufacturers should be notified when lines changes, because changing lines automatically changes manufacturers without triggering notifications. This is used when one property gets its value based on another. For your situation, you need to change manufacturers when you change lines.