Use KVO for NSTextFields that are related to each other

I'm having trouble working with KVO with text fields linked together in a Cocoa application. I got this to work when setting strings in NSTextFields using buttons, but it does not work with bindings. As always, any help from Qaru would be appreciated.

Purpose of my code:

  • link multiple text fields together

  • when a number is entered in one field, other fields automatically update

  • observe changes in text fields

Here is my code for MainClass, which is a subclass of NSObject:

#import "MainClass.h" @interface MainClass () @property (weak) IBOutlet NSTextField *fieldA; @property (weak) IBOutlet NSTextField *fieldB; @property (weak) IBOutlet NSTextField *fieldC; @property double numA, numB, numC; @end @implementation MainClass static int MainClassKVOContext = 0; - (void)awakeFromNib { [self.fieldA addObserver:self forKeyPath:@"numA" options:0 context:&MainClassKVOContext]; [self.fieldB addObserver:self forKeyPath:@"numB" options:0 context:&MainClassKVOContext]; [self.fieldC addObserver:self forKeyPath:@"numC" options:0 context:&MainClassKVOContext]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context != &MainClassKVOContext) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; } if (object == self.fieldA) { if ([keyPath isEqualToString:@"numA"]) { NSLog(@"fieldA length = %ld", [_fieldA.stringValue length]); } } if (object == self.fieldB) { if ([keyPath isEqualToString:@"numB"]) { NSLog(@"fieldB length = %ld", [_fieldB.stringValue length]); } } if (object == self.fieldC) { if ([keyPath isEqualToString:@"numC"]) { NSLog(@"fieldC length = %ld", [_fieldC.stringValue length]); } } } + (NSSet *)keyPathsForValuesAffectingNumB { return [NSSet setWithObject:@"numA"]; } + (NSSet *)keyPathsForValuesAffectingNumC { return [NSSet setWithObject:@"numA"]; } - (void)setNumB:(double)theNumB { [self setNumA:theNumB * 1000]; } - (double)numB { return [self numA] / 1000; } - (void)setNumC:(double)theNumC { [self setNumA:theNumC * 1000000]; } - (double)numC { return [self numA] / 1000000; } - (void)setNilValueForKey:(NSString*)key { if ([key isEqualToString:@"numA"]) return [self setNumA: 0]; if ([key isEqualToString:@"numB"]) return [self setNumB: 0]; if ([key isEqualToString:@"numC"]) return [self setNumC: 0]; [super setNilValueForKey:key]; } @end 

And here is the binding for one of the text fields: enter image description here

+7
source share
1 answer

Monitoring Key Values ​​on NSTextFields

In your implementation of the -awakeFromNib method -awakeFromNib you wrote

 [self.fieldA addObserver:self forKeyPath:@"numA" options:0 context:&MainClassKVOContext]; 

This does not mean that you hope: self.fieldA not compatible with key values for the numA key: if you try to send -valueForKey: or -setValue:forKey: using the @"numA" key to self.fieldA , you will get the following exceptions:

[valueForUndefinedKey:]: this class is not the key to encode for the numA key.

and

[setValue: forUndefinedKey:]: this class is not suitable for encoding the key for the numA key.

As a result , NSTextField instances are not a key compliance for @"numA" , either: the first requirement to be KVO-compatible for some key must be KVC-compatible for this key.

It is, however, KVO-compatible, among other things, stringValue . This allows you to do what I described earlier .

Note None of this has changed the way you configured the bindings in Interface Builder. More on this later.

Problem with monitoring key values ​​on an NSTextField stringValue

Observing the NSTextField value for @"stringValue" works when -setStringValue: is called on an NSTextField . This is the result of internal KVO operations.

A short trip to the internal KVO

When you start observing a key value by observing an object for the first time, the class of the object changes — its pointer isa changes. This can be seen by overriding -addObserver:forKeyPath:options:context:

 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { NSLog(@"%p, %@", self->isa, NSStringFromClass(self->isa)); [super addObserver:observer forKeyPath:keyPath options:options context:context]; NSLog(@"%p, %@", self->isa, NSStringFromClass(self->isa)); } 

In general, the class name changes from Object to NSKVONotifying_Object .

If we called -addObserver:forKeyPath:options:context: in an Object instance with the key @"property" - the key for which Object instances are KVC compatible - the next time -setProperty: call -setProperty: on, our Object instance (in fact, now an NSKVONotifying_Object instance ), the following messages will be sent to the object

  • -willChangeValueForKey: pass @"property"
  • -setProperty: pass @"property"
  • -didChangeValueForKey: pass @"property"

Violation of any of these methods indicates that they are called from the undocumented function _NSSetObjectValueAndNotify .

The relevance of all this is that the -observeValueForKeyPath:ofObject:change:context: method is called on the observer that we added to our Object instance for the key path @"property" from -didChangeValueForKey: Here's the top of the stack trace:

 -[Observer observeValueForKeyPath:ofObject:change:context:] NSKeyValueNotifyObserver () NSKeyValueDidChange () -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] () 

How does this relate to NSTextField and @"stringValue" ?

In the previous setup, you added an observer to the text box on -awakeFromNib . This meant that your text field was already an instance of NSKVONotifying_NSTextField .

Then you press one button or another, which in turn will call -setStringValue into the text box. You were able to observe this change because - as an instance of NSKVONotifying_NSTextField - your text field, actually setStringValue:value

  • willChangeValueForKey:@"stringValue"
  • setStringValue:value
  • didChangeValueForKey:@"stringValue"

As stated above, internally didChangeValueForKey:@"stringValue" all objects that observe the text field value for @"stringValue" are notified that the value for this key has changed in their own implementations -observeValueForKeyPath:ofObject:change:context: In particular, this is true for the object that you added as an observer for the text field in -awakeFromNib .

So, you were able to observe the change in the text field value for @"stringValue" , because you added yourself as a text field observer for this key and because -setStringValue was called in the text field .

So what is the problem?

So far, under the guise of a discussion of "Problems with observing key values ​​on NSTextFields," we have only actually understood the initial sentence

Observing the NSTextField value for @"stringValue" works when -setStringValue: is called on an NSTextField .

And that's great! So what is the problem?

The problem is that -setStringValue: not called in the text field when the user enters OR even after the user has finished editing (for example, by going from the text field). (In addition, -willChangeValueForKey: and -didChangeValueForKey: not called manually. If they were, our KVO would work, but it isn’t.) This means that although our KVO on @"stringValue" works when -setStringValue: It is called on a text field, it does NOT work when the user enters text.

TL; DR : KVO on @"stringValue" NSTextField not good enough as it does not work for user input.

Binding an NSTextField Value to a String

Try using bindings.

Initial setup

Create a sample project with a separate window controller (I used the WindowController declaration WindowController ) complete with XIB. ( Here's the project I'm starting with GitHub. ) In WindowController.m , the stringA property has been stringA to the class extension:

 @interface WindowController () @property (nonatomic) NSString *stringA; @end 

In Interface Builder, create a text box and open the binding inspector:

Bindings inspector

In the Value heading, expand the Value element:

NSControl Value Binding

In the button that appears next to the “Bind” field, the checkbox “Custom default controller” is selected. We want to bind the value of the text field to our instance of WindowController , so choose File Owner instead. When this happens, the "Controller Key" field will be emptied, and the "Model Key Path" field will be changed to "self".

Binding NSControl Value to File's Owner

We want to bind this text field value to our WindowController property of WindowController instance to change the "model key path" to self.stringA :

Binding NSTextField's Value to File's Owner's property stringA

This is where we are done. ( Progress so far on GitHub. ) We have successfully bound the value of the text field to our WindowController stringA property.

Testing

If we set stringA to some value in -init, this value will be displayed in the text box when the window loads:

 - (id)init { self = [super initWithWindowNibName:@"WindowController"]; if (self) { self.stringA = @"hello world"; } return self; } 

Showing 'hello world' in Text Field

And we have already set the bindings in the other direction; after editing, the property of our window controller stringA set in the text field. We can verify this by overriding its setter:

 - (void)setStringA:(NSString *)stringA { NSLog(@"%s: stringA: <<%@>> => <<%@>>", __PRETTY_FUNCTION__, _stringA, stringA); _stringA = stringA; } 

Reply Hazy, Try Again

After entering some text in the text box and clicking the tab, we will see a printout

 -[WindowController setStringA:]: stringA: <<(null)>> => <<some text>> 

It looks great. Why didn’t we talk about this? Here's a bit of a hitch: a persistent pressed tab . Binding the value of a text field to a line does not set the value of the line until editing ends in the text field.

New Hope

However, there is hope! The Cocoa Binding Documentation for NSTextField states that one NSContinuouslyUpdatesValueBindingOption binding option is available for the NSTextField parameter. And now, there is a flag corresponding to this very option in the Bindings Inspector for the value of NSTextField. Go and check the box.

NSTextField's value binding continuously updates stringA

With this change in place, when we make the change, the update for the stringA window controller stringA constantly logs out:

 -[WindowController setStringA:]: stringA: <<(null)>> => <<t>> -[WindowController setStringA:]: stringA: <<t>> => <<th>> -[WindowController setStringA:]: stringA: <<th>> => <<thi>> -[WindowController setStringA:]: stringA: <<thi>> => <<thin>> -[WindowController setStringA:]: stringA: <<thin>> => <<thing>> -[WindowController setStringA:]: stringA: <<thing>> => <<things>> -[WindowController setStringA:]: stringA: <<things>> => <<things >> -[WindowController setStringA:]: stringA: <<things >> => <<things i>> -[WindowController setStringA:]: stringA: <<things i>> => <<things in>> 

Finally, we constantly update the window controller line from the text box. The rest is easy. As a quick proof of concept, add a few more text fields to the window, bind them to stringA and configure them to be constantly updated. You currently have three synchronized NSTextField s! Here is a project with three synchronized text fields.

Rest of the way

You want to customize three text fields displaying numbers that have something to do with each other. Since we are dealing with numbers now, we will remove the stringA property from WindowController and replace it with numberA , numberB and numberC :

 @interface WindowController () @property (nonatomic) NSNumber *numberA; @property (nonatomic) NSNumber *numberB; @property (nonatomic) NSNumber *numberC; @end 

Then we associate the first text field with number A with the file owner, the second with number B, etc. Finally, we just need to add a property that represents the quantity that is represented in these various ways. Let me call this value quantity .

 @interface WindowController () @property (nonatomic) NSNumber *quantity; @property (nonatomic) NSNumber *numberA; @property (nonatomic) NSNumber *numberB; @property (nonatomic) NSNumber *numberC; @end 

We will need constant conversion factors to convert from quantity units to numberA units, etc., so add

 static float convertToA = 1000.0f; static float convertToB = 573.0f; static float convertToC = 720.0f; 

(Of course, use numbers that are relevant to your situation.) With this help, we can implement accessors for each of the numbers:

 - (NSNumber *)numberA { return [NSNumber numberWithFloat:self.quantity.floatValue * convertToA]; } - (void)setNumberA:(NSNumber *)numberA { self.quantity = [NSNumber numberWithFloat:numberA.floatValue * 1.0f/convertToA]; } - (NSNumber *)numberB { return [NSNumber numberWithFloat:self.quantity.floatValue * convertToB]; } - (void)setNumberB:(NSNumber *)numberB { self.quantity = [NSNumber numberWithFloat:numberB.floatValue * 1.0f/convertToB]; } - (NSNumber *)numberC { return [NSNumber numberWithFloat:self.quantity.floatValue * convertToC]; } - (void)setNumberC:(NSNumber *)numberC { self.quantity = [NSNumber numberWithFloat:numberC.floatValue * 1.0f/convertToC]; } 

All of the various number accessors are now just indirect mechanisms for accessing quantity and are ideal for bindings. There is only one more thing left: we need to make sure that observers repeat all numbers with every change in quantity :

 + (NSSet *)keyPathsForValuesAffectingNumberA { return [NSSet setWithObject:@"quantity"]; } + (NSSet *)keyPathsForValuesAffectingNumberB { return [NSSet setWithObject:@"quantity"]; } + (NSSet *)keyPathsForValuesAffectingNumberC { return [NSSet setWithObject:@"quantity"]; } 

Now, whenever you enter text in one of the text fields, the rest is updated accordingly. Here's the final version of the project on GitHub .

+30
source

All Articles