objective-ccocoanstextfieldnscontrol

ControlTextDidChange not working for setting string of NSTextField


I'm trying to find a method that monitors the text of NSTextField for changes. I tried the delegate method of -(void)controlTextDidChange:(NSNotification *)obj but it only works when the user types into the text field. If the text field string is programmatically set, such as with a button, the controlTextDidChange doesn't work.

Is there a method or another approach that I can use to monitor the contents of a NSTextField for changes?

My ButtonText class (set as delegate for the NSTextField):

#import "ButtonText.h"

@interface ButtonText ()

@property (weak) IBOutlet NSTextField *buttonField;

@end

@implementation ButtonText

- (IBAction)buttonTextA:(id)sender {
    [_buttonField setStringValue:@"text A here"];
}

- (IBAction)buttonTextB:(id)sender {
    [_buttonField setStringValue:@"and text B stuff"];
}

- (void)controlTextDidChange:(NSNotification *)obj {
    NSLog(@"controlTextDidChange: %@", _buttonField.stringValue);
}

@end

The XIB showing the buttons and text field: enter image description here


Solution

  • One approach is to use KVO. In particular, add the ButtonText instance as an observer of buttonField's stringValue.

    In more detail, in your file ButtonText, once the @property IBOutlet buttonField has been set (i.e. if ButtonText is an NSWindowController subclass, in -windowDidLoad, and if ButtonText is an NSViewController subclass in -loadView), call

    [self.buttonField addObserver:self
                       forKeyPath:@"stringValue"
                          options:0
                          context:&ButtonTextKVOContext];
    

    Define ButtonTextKVOContext previously in the file as follows:

    static int ButtonTextKVOContext = 0;
    

    Then override observeValueForKeyPath:ofObject:change:context: as follows:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if (context != &ButtonTextKVOContext) {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
            return;
        }
    
        if (object == self.buttonField) {
            if ([keyPath isEqualToString:@"stringValue"]) {
                NSLog(@"controlTextDidChange: %@", _buttonField.stringValue);
            }
        }
    }
    

    Edit

    Since ButtonText is not a subclass of NSWindowController or NSViewController, we'll use a slightly different approach. As before, we'll want to start observing "once the @property IBOutlet buttonField has been set". To do this, synthesize the property buttonField to be the member variable mButtonField writing

    @synthesize buttonField = mButtonField;
    

    and override buttonField's setter as follows:

    - (void)setButtonField:(NSTextField *)buttonField
    {
        [self stopObservingButtonField];
        mButtonField = buttonField;
        [self startObservingButtonField];
    }
    

    We need to make sure that ButtonText stops observing the button field when it deallocates as well, so override -dealloc as follows:

    - (void)dealloc
    {
        [self stopObservingButtonField];
    }
    

    It remains to define the methods -stopObservingButtonField and -startObservingButtonField:

    - (void)stopObservingButtonField
    {
        if (mButtonField) {
            [mButtonField removeObserver:self
                              forKeyPath:@"stringValue"
                                 context:&ButtonTextKVOContext];
        }
    }
    
    - (void)startObservingButtonField
    {
        if (mButtonField) {
            [self.buttonField addObserver:self
                               forKeyPath:@"stringValue"
                                  options:0
                                  context:&ButtonTextKVOContext];
        }
    }
    

    As a result of this arrangement, we must never set the mButtonField variable outside of the -setButtonField: method. (This isn't quite true, but if we do set mButtonField we must be sure to first of all stop observing its old value's @"stringValue" key path and start observing its new value's @"stringValue" key path. Doing this rather than simply calling -setButtonField: would very likely simply constitute code repetition and not be worthwhile.)

    For reference, check out Apple's documentation on the NSKeyValueObserving protocol.