9

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

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
zorn
  • 438
  • 1
  • 5
  • 15

2 Answers2

6

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
  • 11,915
  • 4
  • 44
  • 58
  • 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 Mar 04 '09 at 22:04
  • 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 Mar 04 '09 at 22:05
  • With some early testing this works. I already had most of the observation stuff in place too (for undo support). Thanks again. – zorn Mar 04 '09 at 22:12
  • Hrm... never mind. That FAQ states "keyPathsForValuesAffectingValueForKey: does not allow key-paths that include a to-many relationship" – Dave Dribin Mar 04 '09 at 22:14
  • 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 Mar 04 '09 at 23:34
  • If your observer is an NSManagedObject, don't forget to stopObserving in willTurnIntoFault and prepareForDeletion. Without this, you'll likely experience crashes. – David Aug 24 '14 at 18:35
0

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:@"InvoiceLineItems.@sum.cost"];
}
Elise van Looij
  • 4,162
  • 3
  • 29
  • 52