Search code examples
nsuserdefaultscocoa-bindingsnsmenuitem

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


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.


Solution

  • 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.