Search code examples
objective-ccocoacocoa-bindingskey-value-observing

Cocoa: bind to composite object and observe child properties


Is it possible to bind to a custom object and get notified when specific properties of that object change?

Example:

Each sales rep object has a reference to a sales statistics object with stats for car and truck sales. A custom NSView is used to draw both, car and truck sales in one graphic.

The custom view needs access to the full statistics object, so I bound it to salesRep.salesStats.

However, when I change the carSales property I need a way for the view to update. Currently it doesn't update, because it is bound to the parent object.

I'd like to avoid establishing bindings for each sales type (this is only an example, my specific case is more complex).

@interface SBSalesRep : NSObject {

@property (strong) SBSalesStatistics *salesStats;
}

@interface SBSalesStatistics : NSObject
{
@property (assign) NSInteger carSales;
@property (assing) NSInteger truckSales;
}

@interface SBProgressView : NSView
{
@property (strong) SBSalesStatistics *statsToDisplay;
}

// Setup
SBSalesRep *salesRep = [SBSalesRep new];

// Bind to stats object, because we need car and tuck sales:
[progressView bind:@"statsToDisplay" 
          toObject:salesRep 
       withKeyPath:@"salesStats"  // How to get notified of property changes here?
           options:nil];

// This needs to trigger an update in the statistics view
salesRep.salesStats.carSales = 50;

// I tried this, but it does not work:
[salesRep willChangeValueForKey:@"salesStatistics"];
salesRep.salesStats.carSales = 50;
[salesRep didChangeValueForKey:@"salesStatistics"];

Solution

  • You said:

    // I tried this, but it does not work:
    [salesRep willChangeValueForKey:@"salesStatistics"];
    salesRep.salesStats.carSales = 50;
    [salesRep didChangeValueForKey:@"salesStatistics"];
    

    My guess would be that this doesn't work because the key you're notifying for is salesStatistics but the key you're binding to is salesStats. If those keys were the same, I would expect this approach to work.

    Beyond that, a better approach might be to add dependencies like this:

    @implementation SBSalesRep
    
    + (NSSet *) keyPathsForValuesAffectingSalesStats
    {
        return [NSSet setWithObjects: @"salesStats.carSales", @"salesStats.truckSales", nil];
    }
    
    @end
    

    This will cause any observation (binding or otherwise) of the key salesStats to also implicitly observe salesStats.carSales and salesStats.truckSales, and should achieve the desired effect without having to notify manually with -will/didChangeValueForKey:.