Question

One of the things I've always been unsure of is how to properly handle communication between a custom NSControl subclass and an NSCell subclass. Throughout my introduction to Cocoa, I've seen it mentioned several times how the parent control provides many of the same methods, accessors, and mutators as the child cell's/cells' implementation. For example, the NSControl class and NSCell class both have -isEnabled and -setEnabled: in their header files:

NSControl.h

- (BOOL)isEnabled;
- (void)setEnabled:(BOOL)flag;

NSCell.h

- (BOOL)isEnabled;
- (void)setEnabled:(BOOL)flag;

I understand that the NSControl class provides "cover" methods for most of the properties found in NSCell. What I'm more interested in knowing is: how are they implemented? Or even better, how should one implement his/her own subclasses' shared properties? Obviously, only Apple's engineers really know what's happening on the inside of their frameworks, but I thought maybe somebody could shed some light on the best way to mimic Apple's cover method approach in a nice clean way.

I'm horrible at explaining stuff, so I'll provide an example of what I'm talking about. Say I've subclassed NSControl like so:

BSDToggleSwitch.h

#import "BSDToggleSwitchCell.h"

@interface BSDToggleSwitch : NSControl

@property (nonatomic, strong) BSDToggleSwitchCell *cell;
@property (nonatomic, assign) BOOL sharedProp;

@end

And I've subclassed NSActionCell:

BSDToggleSwitchCell.h

#import "BSDToggleSwitch.h"

@interface BSDToggleSwitchCell : NSActionCell

@property (nonatomic, weak) BSDToggleSwitch *controlView;
@property (nonatomic, assign) BOOL sharedProp;

@end

As you can see, they both share a property called sharedProp.

My question is this: What's the standard way to effectively keep the shared property synchronized between the control and the cell? This may seem like a subjective question, and I suppose it is, but I'd like to think that there is a most-of-the-time "best way" to do this.

I've used all sorts of different methods in the past, but I'd like to narrow down the ways in which I handle it, and only use the techniques which provide the best data integrity with the lowest overhead. Should I use bindings? What about implementing custom mutators which call their counterparts' matching method? KVO? Am I a lost cause?

Here are some of the things I've done in the past (some or all of which could be totally whacky or straight-up wrong):

  1. Cocoa Bindings — I'd just bind the control's property to the cell's property (or vice versa):

    [self bind:@"sharedProp" toObject:self.cell withKeyPath:@"sharedProp" options:nil];
    

    This seems like a pretty good approach, but which object would you bind to/from? I've read all of the KVO/KVC/Bindings documentation, but I've never really picked up on the importance of the binding's directionality when the property should be the same in either case. Is there a general rule?

  2. Send Messages from Mutators — I'd send the cell a message from the control's mutator:

    - (void)setSharedProp:(BOOL)sharedProp
    {
        if ( sharedProp == _sharedProp )
            return;
    
        _sharedProp = sharedProp;
    
        [self.cell setSharedProp:sharedProp];
    }
    

    Then I'd do the same thing in the cell's implementation:

    - (void)setSharedProp:(BOOL)sharedProp
    {
        if ( sharedProp == _sharedProp )
            return;
    
        _sharedProp = sharedProp;
    
        [self.controlView setSharedProp:sharedProp];
    }
    

    This also seems fairly reasonable, but it also seems more prone to errors. If one object sends a message to the other without a value check, an infinite loop could happen pretty easily, right? In the examples above, I added checks for this reason, but I'm sure there's a better way.

  3. Observe and Report — I would observe property changes in each object's implementation:

    static void * const BSDPropertySyncContext = @"BSDPropertySyncContext";
    
    - (void)observeValueForKeyPath:(NSString *)keyPath 
                          ofObject:(id)object 
                            change:(NSDictionary *)change 
                           context:(void *)context
    {
        if ( context == BSDPropertySyncContext && [keyPath isEqualToString:@"sharedProp"] ) {
    
            BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
    
            if ( newValue != self.sharedProp ) {
                [self setSharedProp:newValue];
            }
    
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    

    Once again, this seems doable, but I don't like having to write an if statement for every single shared property. Along the lines of observation, I've also sent notifications, but since the control-cell relationship is about as "one-to-one" as one can get (sweet pun intended), that just seems silly.

Again, I know this is a bit subjective, but I'd really appreciate some guidance. I've been learning Cocoa/Objective-C for awhile now, and this has bothered me since the beginning. Knowing how others handle property syncing between controls and cells could really help me out!

Thanks!

Was it helpful?

Solution

First, note that NSCell mostly exists because of performance issues from the NeXT days. There has been a slow migration away from NSCell, and you generally shouldn't create new ones unless you need to interact with something that demands them (such as working with an NSMatrix, or if you're doing something that looks a lot like an NSMatrix). Note the changes to NSTableView since 10.7 to downplay cells, and how NSCollectionViewItem is a full view controller. We now have the processing power to just use views most of the time without needing NSCell.

That said, typically the control just forwards messages to the cell. For instance:

- (BOOL)sharedProp {
  return self.cell.sharedProp;
}

- (void)setSharedProp:(BOOL)sharedProp {
  [self.cell setSharedProp:sharedProp];
}

If KVO is a concern, you can still hook it up with keyPathsForValuesAffectingValueForKey: (and its relatives). For instance, you can do something like:

- (NSSet *)keyPathsForValuesAffectingSharedProp {
   return [NSSet setWithObject:@"cell.sharedProp"];
}

That should let you observe sharedProp and be transparently forwarded changes to cell.sharedProp.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top