对绑定在一起的NSTextField使用KVO [英] Use KVO for NSTextFields that are bound together

查看:334
本文介绍了对绑定在一起的NSTextField使用KVO的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我无法获取KVO使用在Cocoa应用程序中绑定在一起的文本字段。我已经得到这个工作时设置字符串在NSTextFields按钮,但它不工作与绑定。一如既往,Stack Overflow的任何帮助将非常感谢。



我的代码的目的是:




  • 将多个文本字段绑定在一起


  • 当一个字段输入数字时,



这里是我的代码MainClass是一个NSObject子类:

  #importMainClass.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:@numAoptions:0 context:& MainClassKVOContext];
[self.fieldB addObserver:self forKeyPath:@numBoptions:0 context:& MainClassKVOContext];
[self.fieldC addObserver:self forKeyPath:@numCoptions: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

其中一个文本字段:

解决方案

NSTextFields的键值观察



-awakeFromNib 方法的实现,你写了

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

这不会做你想要的: self。 fieldA 不是 。)我们已成功将文本字段的值绑定到我们的 WindowController 的属性 stringA



测试它



如果我们设置 stringA 到-init中的某个值,当窗口加载时,该值将显示在文本字段中:

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



现在,我们已经在另一个方向设置了绑定;在文本字段中结束编辑时,设置我们的窗口控制器的属性 stringA 。我们可以通过覆盖它的setter来检查:

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



回复阴霾,请重试



在文本字段中输入一些文本并按Tab键后,我们将看到打印出来的

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

这看起来不错。为什么我们一直没有谈论这一切?这里有一点点麻烦:幽灵按Tab键。将文本字段的值绑定到字符串不会设置字符串值,直到文本字段中的编辑结束。



A新希望



但是,仍然有希望!



剩余部分



您要设置三个文本字段,显示彼此之间具有某种关系的数字。由于我们现在处理数字,我们将从 WindowController 中删除​​ stringA 的属性,并将其替换为 numberA numberB numberC

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



接下来我们将第一个文本字段绑定到File的Owner上的numberA,第二个到numberB,等等。最后,我们只需要添加一个属性,它是以这些不同方式表示的数量。让我们调用该值 quantity

  @interface WindowController()
@property(nonatomic)NSNumber * quantity;

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



我们需要常量转换因子从 numberA 的单位添加 code> $ ,因此添加

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

(当然,使用与你的情况相关的数字)。为每个数字实现访问器:

   - (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] ;
}

所有不同的数字访问器现在只是访问 quantity ,是完美的绑定。只有一个额外的事情要做:我们需要确保每当 quantity 更改时,观察者重新填充所有数字:

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

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

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

现在,无论何时输入其中一个文本字段, 。 这是GitHub上的项目的最终版本


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 written

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

This doesn't do what you're hoping it will: self.fieldA is not key-value coding compliant for the key numA: if you try sending -valueForKey: or -setValue:forKey: with the key @"numA" to self.fieldA, you'll get the following exceptions:

[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key numA.

and

[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key numA.

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 the NSTextField. 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:

- (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 name of the class changes from Object to NSKVONotifying_Object.

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

  1. -willChangeValueForKey: passing @"property"
  2. -setProperty: passing @"property"
  3. -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 of Object 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 your previous setup, you were adding an observer to your text field on -awakeFromNib. This meant that your text field was already an instance of NSKVONotifying_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 of NSKVONotifying_NSTextField--your text field, upon receiving setStringValue:value actually received

  1. willChangeValueForKey:@"stringValue"
  2. setStringValue:value
  3. 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

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

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 an NSTextField 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.) In WindowController.m added a property stringA in a class extension:

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

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 property stringA so change the "Model Key Path" to self.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 property stringA.

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:

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

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:

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

Reply Hazy, Try Again

After typing some text into the text field and pressing tab, we'll see printed out

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

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 an NSTextField is NSContinuouslyUpdatesValueBindingOption. 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:

-[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'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 NSTextFields! 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 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

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.

@interface WindowController ()
@property (nonatomic) NSNumber *quantity;

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

We'll need the constant conversion factors to transform from the units of quantity to the units of numberA and so forth, so add

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

(Of course, use the numbers that are relevant to your situation.) With this much, we can implement the 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 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 whenever quantity is changed:

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

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

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

Now, whenever you type into one of the textfields, the others are updated accordingly. Here's the final version of the project on GitHub.

这篇关于对绑定在一起的NSTextField使用KVO的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆