State of NSMenuItem bound to boolean in NSUserDefaults not staying in sync

1.8k Views Asked by At

I have an NSMenuItem titled "Word Wrap" in my main menu (MainMenu.xib). Its value is bound to my shared user defaults controller, also instantiated in the XIB. It also sends the following action when selected:

- (IBAction)toggleWordWrap:(id)sender {
    NSUserDefaultsController *ctrlr = [NSUserDefaultsController sharedUserDefaultsController];
    if ([[[ctrlr values] valueForKey:@"wordWrapIsEnabled"] boolValue]) {
        // turn on word wrap
    } else {
        // turn off word wrap
    }
}

In my app delegate's +initialize method, I populate the standard user defaults with default values:

+ (void)initializeDefaults {
    NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:NO], @"wordWrapIsEnabled",
                             // etc.
                             nil];
    NSUserDefaultsController *ctrlr = [NSUserDefaultsController sharedUserDefaultsController];
    [ctrlr setInitialValues:defaults];
}

My problem is that my NSMenuItem's state is not staying in sync with my user defaults. Here is a timeline of what happens:

App launch:

  • Word Wrap menu item not checked
  • wordWrapIsEnabled is NO
  • Word wrap is OFF

1st time Word Wrap is selected:

  • Word Wrap menu item checked
  • wordWrapIsEnabled is NO (BZZZT WRONG)
  • Word wrap is OFF (BZZZT WRONG)

2nd time Word Wrap is selected:

  • Word Wrap menu item not checked
  • wordWrapIsEnabled is YES (BZZZT WRONG)
  • Word Wrap is ON (BZZZT WRONG)

Repeat flip-flop ad infinitum.

I've checked to make sure there is nothing else in my project that accesses wordWrapIsEnabled. Could there be a race condition between the invocation of the selector and the setting of wordWrapIsEnabled via the binding? I've been assuming that the bound value gets set first.

2

There are 2 best solutions below

2
On BEST ANSWER

When you click a menu item with a bound state (or value) property, the menu item both triggers its action and flips the bound value. And the order of these two operations does not seem to be guaranteed, see the following thread on Cocoa Builder:

Thanks, I am not absolutely sure because I did several changes in my project but I think that this can be considered a bug of 10.5 sdk, because it started to happen when I started to compile for it. The (almost) same project when it was targeted for Tiger always changed the bound value before the target-action was executed, regardless if it was a button or a menuItem. Apparently this consistency has been broken in Leopard. I may post a bug report after some testing to confirm it.

There’s also a related Radar bug report saying that menu items should not flip the bound value automatically. This is probably too late as an answer to your question, but hopefully will help next time somebody runs into this issue.

0
On

When you're using Cocoa bindings to Shared User Defaults for a NSMenuItem, you should stop using the selector for the NSMenuItem and instead use key-value observing to determine when the value has changed and then act appropriately.

In this example, I've got a useTransparency value name that an NSMenuItem is bound to. In my controller's init, I register to receive updates to this value:

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [userDefaults addObserver:self
                   forKeyPath:@"useTransparency"
                      options:NSKeyValueObservingOptionNew
                      context:NULL];

Then later I implement the observer method:

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context
{
    NSLog(@"KVO: %@ changed property %@ to value %@", object, keyPath, change);

    if ([keyPath compare:@"useTransparency"] == NSOrderedSame)
    {
        BOOL isTransparent = [[change valueForKey:@"new"] boolValue];
        [self setTransparency:isTransparent];
    }
}

In particular, I do not bind a selector for the NSMenuItem at all - I just let key-value observing do the job. If you bind to the selector, you run into the problem of trying to guess when the value will change versus the selector being fired off. Avoid that whole problem completely by just using the bindings system rather than a mix of the two.