views:

453

answers:

2

So I have two objects, Invoice and InvoiceLineItem. InvoiceLineItem has a property called cost and it is dynamically created based on other properties. To help with the KVO/bindings I use:

+ (NSSet *)keyPathsForValuesAffectingCost {
    return [NSSet setWithObjects:@"lineItemType", @"serviceCost", @"hourlyRate", @"timeInSeconds", @"productCost", @"quantityOfProduct", @"mileageCost", @"milesTraveled", nil];
}

This works great. When I edit a property like serivceCost the main cost in the Table View updates fine.

In the Invoice object I have an NSMutableArray of InvoiceLineItems. Invoice has a similar property called totalCost. It is calculated by iterating over the line items and as long as the line item isn't marked as deleted(which I do for syncing reasons) it adds up the costs and creates the totalCost.

Now my question/issue. How do I set up Invoice's totalCost so that it works with KVO/bindings when one of the line item's costs has changed?

I tried setting up:

+ (NSSet *)keyPathsForValuesAffectingTotalCost {
    return [NSSet setWithObjects:@"lineItems.cost", nil];
}

but it doesn't work. I end up with an error in the console: [<NSCFArray 0x1499ff40> addObserver:forKeyPath:options:context:] is not supported. Key path: cost

+3  A: 

I don't believe to-many relationships are supported for automatic KVO propogation. The documentation doesn't say explicity one way or the other, but from what I know of KVO in general, observing subkeys of a to-many relationship tends to be non-trivial.

The way I would approach this would be to manually observe the cost property of each InvoiceLineItem object, by implementing the to-many KVC accessors for the lineItems property on the Invoice class doing an addObserver/removeObserver call in the insert/remove methods, respectively, and then trigger the totalCost change manually using willChangeValueForKey:/didChangeValueForKey:. So something like this (roughly sketched code, disclaimers etc.):

- (void)insertObject:(InvoiceLineItem*)newItem inLineItemsAtIndex:(unsigned)index
{
    [newItem addObserver:newItem forKeyPath:@"cost" options:0 context:kLineItemContext];
    [lineItems insertObject:newItem atIndex:index];
}

- (void)removeObjectFromLineItemsAtIndex:(unsigned)index
{
    [[lineItems objectAtIndex:index] removeObserver:self forKeyPath:@"cost"];
    [lineItems removeObjectAtIndex:index];
}

- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
    if (context == kLineItemContext)
    {
        [self willChangeValueForKey:@"totalCost"];
        [self didChangeValueForKey:@"totalCost"];
    }
}
Brian Webster
I was pretty close to this kind of implementation myself but it's good to hear it come from someone else. Question though, the stacking of will/didChange ... why not just call didChange?
zorn
I think it should work, if the underlying objects implement it correctly. For example, the Core Data FAQ shows something just like this:http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdFAQ.html#//apple_ref/doc/uid/TP40001802-SW3
Dave Dribin
With some early testing this works. I already had most of the observation stuff in place too (for undo support). Thanks again.
zorn
Hrm... never mind. That FAQ states "keyPathsForValuesAffectingValueForKey: does not allow key-paths that include a to-many relationship"
Dave Dribin
The main purpose for it is so the old value can be included in the change dictionary that's sent to observers. That doesn't really apply in this case, but I think KVO will complain if the didChange call isn't balanced with a willChange beforehand.
Brian Webster
A: 

You might try a shorter solution.

Add to the header file:

@property (retain, readonly) NSDecimalNumber *accountBalance;

Add to the implementation file

- (NSDecimalNumber *)totalCost
{
    return [self valueForKeyPath:@"[email protected]"];
}
Elise van Looij