mzf
mzf

Reputation: 430

Subclassing NSPopUpButton to add a bindable property

I'm trying to add a bindable property to a custom NSPopUpButton subclass.

I've created a "selectedKey" property, which is meant to store a NSString associated with selected menu item.

In control init, I set self as button target and an action for the button (valueChanged:), which in turn sets "selectedKey" in accordance with user selection:

@interface MyPopUpButton : NSPopUpButton {
    NSMutableDictionary *_items;
    NSString *_selectedKey;    
}

@property(nonatomic, readwrite, copy) NSString* selectedKey;
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key;

@end

@implementation MyPopUpButton
- (instancetype)initWithFrame:(NSRect)frameRect {

    self = [super initWithFrame:frameRect];
    if (self) {

        _items = [NSMutableDictionary new];
        [NSObject exposeBinding:@"selectedKey"];
        [super setTarget:self];
        [super setAction:@selector(valueChanged:)];

    }
    return self;

}

- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key {

    [super addItemWithTitle:title];
    [_items setValue:title forKey:key];

}

- (void)valueChanged:(id)sender {

    for (NSString *aKey in [_items allKeys]) {
        if ([[_items valueForKey:aKey] isEqualToString:[self titleOfSelectedItem]]) {
            self.selectedKey = aKey;
        }
    }

}

- (void)setSelectedKey:(NSString *)selectedKey {

    [self willChangeValueForKey:@"selectedKey"];
    _selectedKey = selectedKey;
    [self didChangeValueForKey:@"selectedKey"];

    [self selectItemWithTitle:[_items valueForKey:selectedKey]];

}
@end

This seems to work as expected: "selectedKey" property is changed when user changes PopUpButton selection.

Unfortunately, trying to bind this property, doesn't work.

[selectButton bind:@"selectedKey" toObject:savingDictionary withKeyPath:key options:@{NSContinuouslyUpdatesValueBindingOption : @YES }]

When selection is changed bind object is not updated accordingly.

What am I doing wrong?

Upvotes: 0

Views: 391

Answers (1)

Paul Patterson
Paul Patterson

Reputation: 6928

I've created a "selectedKey" property, which is meant to store an NSString associated with selected menu item.

Bindings is definitely the way to go here, but your use of bind:toObject:withKeyPath:options is incorrect.

The value that you pass to the first argument must be one of the predefined values made available by Apple for that particular control. For NSPopUpButton objects, the available values are documented in the NSPopUpButton Bindings Reference. When you look through this document you'll see that there is no selectedKey option. There is however a selectedValue which has the following description:

An NSString that specifies the title of the selected item in the NSPopUpButton.

Thus the correct way to set up the binding is as follows:

[self.btn bind:@"selectedValue"
      toObject:self
   withKeyPath:@"mySelectedString"
       options:nil];

This is all you need to do: when the action selector is fired the property stored at the keyPath you passed in as the third argument will already have been updated. This means that you can (i) get rid of the setSelectedKey method entirely, (ii) remove exposeBinding line, and (iii) remove the code within valueChanged: - Cocoa has already done this bit.

The example below implements just two methods, but, if I've understood your intentions, they should be all you need:

- (void)awakeFromNib {
    self.btn.target = self;
    self.btn.action = @selector(popUpActivity:);

    [self.btn bind:@"selectedValue"
          toObject:self
       withKeyPath:@"mySelectedString"
           options:nil];

    // I've added a couple of additional bindings here; they're
    // not required, but I thought they'd be instructive.
    [self.btn bind:@"content"
          toObject:self
       withKeyPath:@"myItems"
           options:nil];

    [self.btn bind:@"selectedIndex"
          toObject:self
       withKeyPath:@"mySelectedIndex"
           options:nil];

    // Now that you've set the bindings up, use them!
    self.myItems = @[@"Snow", @"Falling", @"On", @"Cedars"];
    self.mySelectedIndex = @3; // "Cedars" will be selected on startup
    // no need to set value of mySelectedString, because it will be
    // updated automatically by the selectedIndex binding.
    NSLog("%@", self.mySelectedString) // -> "Cedars"

}

- (void)popUpActivity:(id)sender {
    NSLog(@"value of <selectedIndex> -> %@", self.mySelectedIndex);
    NSLog(@"value of <selectedString> -> %@", self.mySelectedString);
}

A final point worth making is that none of the above should be a part of an NSPopUpButton subclass. It looks like you can - and therefore should - do everything you need to do without a custom subclass of this control. In my demo-app the code above belongs to the ViewController class, you should try doing this also.

Upvotes: 0

Related Questions