I'm having trouble getting KVO working with text fields that are bound together in a Cocoa app. I have gotten this to work when setting strings in NSTextFields with buttons but it is not working with bindings. As always, any help from Stack Overflow would be greatly appreciated.
Purpose of my code is to:
bind several text fields together
when a number is input in one field, have the other fields automatically update
observe the changes in the text fields
Here's my code for MainClass which is an NSObject subclass:
#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:
Key-Value Observing on NSTextFields
In your
-awakeFromNib
method's implementation, you've writtenThis doesn't do what you're hoping it will:
self.fieldA
is not key-value coding compliant for the keynumA
: if you try sending-valueForKey:
or-setValue:forKey:
with the key@"numA"
toself.fieldA
, you'll get the following exceptions:and
As a result, the
NSTextField
instances are not key-value observing compliant for@"numA"
, either: the first requirement to be KVO-compliant for some key is to be KVC-compliant for that key.It is, however, KVO-compliant for, among other things,
stringValue
. This allows you to do what I described earlier.Note: None of this is altered by the way that you've set up bindings in Interface Builder. More on that later.
The Trouble With Key-Value Observing on NSTextField's stringValue
Observing an
NSTextField
's value for@"stringValue"
works when-setStringValue:
gets called on theNSTextField
. This is a result of the internals of KVO.A Brief Trip Into KVO Internals
When you begin observing an key-value observing an object for the first time, the object's class is changed--its
isa
pointer is changed. You can see this happening by overriding-addObserver:forKeyPath:options:context:
In general, the name of the class changes from
Object
toNSKVONotifying_Object
.If we had called
-addObserver:forKeyPath:options:context:
on an instance ofObject
with with the key path@"property"
--a key for which instances ofObject
are KVC-compliant--when next we call-setProperty:
on our instance ofObject
(in fact, now an instance ofNSKVONotifying_Object
), the following messages will be sent to the object-willChangeValueForKey:
passing@"property"
-setProperty:
passing@"property"
-didChangeValueForKey:
passing@"property"
Breaking within any of these methods reveal that they're called from the undocumented function
_NSSetObjectValueAndNotify
.The relevance of all of this is that the method
-observeValueForKeyPath:ofObject:change:context:
is called on the observer that we added to our instance ofObject
for the key path@"property"
from-didChangeValueForKey:
. Here's the top of the stack trace:How does this relate to
NSTextField
and@"stringValue"
?In your previous setup, you were adding an observer to your text field on
-awakeFromNib
. This meant that your text field was already an instance ofNSKVONotifying_NSTextField
.You would then press one or another button which in turn would call
-setStringValue
on your text field. You were able to observe this change because--as an instance ofNSKVONotifying_NSTextField
--your text field, upon receivingsetStringValue:value
actually receivedwillChangeValueForKey:@"stringValue"
setStringValue:value
didChangeValueForKey:@"stringValue"
As above, from within
didChangeValueForKey:@"stringValue"
, all the objects which are observing the text field's value for@"stringValue"
are notified that the value for this key has changed in their own implementations of-observeValueForKeyPath:ofObject:change:context:
. In particular, this is true for the the object which you added as an observer for the text field in-awakeFromNib
.In summary, you were able to observe the change in the text field's value for
@"stringValue"
because you added yourself as an observer of the text field for that key and because-setStringValue
was called on the text field.So What's The Problem?
So far under the guise of discussing "The Trouble With Key-Value Observing on NSTextFields" we've only actually made sense of the opening sentence
And that sounds great! So what's the problem?
The problem is that
-setStringValue:
does not get called on the text field as the user is typing into it OR even after the user has ended editing (by tabbing out of the text field, for example). (Furthermore,-willChangeValueForKey:
and-didChangeValueForKey:
are not called manually. If they were, our KVO would work; but it doesn't.) This means that while our KVO on@"stringValue"
works when-setStringValue:
is called on the text field, it does NOT work when the user herself enters text.TL;DR: KVO on the
@"stringValue"
of anNSTextField
isn't good enough since it doesn't work for user input.Binding An NSTextField's Value To A String
Let's try using bindings.
Initial Setup
Create an example project with a separate window controller (I've used the creative name
WindowController
) complete with XIB. (Here's the project I'm starting from on GitHub.) InWindowController.m
added a propertystringA
in a class extension:In Interface Builder, create a text field and open the Bindings Inspector:
Under the "Value" header, expand the "Value" item:
The pop-up button next to the "Bind to" checkbox presently has "Shared User Defaults Controller" selected. We want to bind the text field's value to our
WindowController
instance., so select "File's Owner" instead. When this happens, the "Controller Key" field will be emptied and the "Model Key Path" field will be changed to "self".We want to bind this text field's value to our
WindowController
instance's propertystringA
so change the "Model Key Path" toself.stringA
:At this point, we are done. (Progress so far on GitHub.) We have successfully bound the text field's value to our
WindowController
's propertystringA
.Testing It Out
If we set
stringA
to some value in -init, that value will show up in the text field when the window loads:And already, we have set up bindings in the other direction as well; upon ending editing in the text field, the our window controller's property
stringA
is set. We can check this by overriding it's setter:Reply Hazy, Try Again
After typing some text into the text field and pressing tab, we'll see printed out
This looks great. Why haven't we been talking about this all along??? There's a bit of a hitch here: the pesky pressing tab thing. Binding a text field's value to a string does not set the string value until editing has ended in the text field.
A New Hope
However, there is still hope! The Cocoa Binding Documentation for
NSTextField
states that one binding option available for anNSTextField
isNSContinuouslyUpdatesValueBindingOption
. And lo and behold, there is a checkbox corresponding to this very option in the Bindings Inspector for NSTextField's value. Go ahead and check that box.With this change in place, as we type things in, the update to the window controller's
stringA
property is continuously logged out:Finally, we're continuously updating the window controller's string from the text field. The rest is easy. As a quick proof of concept, add a couple more text fields to the window, bind them to stringA and set them to update continuously. You at this point have three synchronized
NSTextField
s! Here's the project with three synchronized text fields.The Rest of the Way
You're wanting to setup three textfields that display numbers that have some relationship to each other. Since we're dealing with numbers now, we'll remove the property
stringA
fromWindowController
and replace it withnumberA
,numberB
andnumberC
:Next we'll bind the first text field to numberA on File's Owner, the second to numberB, and so on. Finally we just need to add a property which is the quantity which is being represented in these different ways. Let's call that value
quantity
.We'll need the constant conversion factors to transform from the units of
quantity
to the units ofnumberA
and so forth, so add(Of course, use the numbers that are relevant to your situation.) With this much, we can implement the accessors for each of the numbers:
All of the different number accessors are now just indirect mechanisms for accessing
quantity
, and are perfect for bindings. There is only one additional thing that remains to be done: we need to make sure that observers repoll all of the numbers wheneverquantity
is changed:Now, whenever you type into one of the textfields, the others are updated accordingly. Here's the final version of the project on GitHub.