8

我无法让 KVO 处理在 Cocoa 应用程序中绑定在一起的文本字段。在使用按钮在 NSTextFields 中设置字符串时,我已经让它工作了,但它不适用于绑定。与往常一样,我们将不胜感激 Stack Overflow 的任何帮助。

我的代码的目的是:

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

  • 在一个字段中输入数字时,让其他字段自动更新

  • 观察文本字段的变化

这是我的 MainClass 代码,它是一个 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

这是其中一个文本字段的绑定: 在此处输入图像描述

4

1 回答 1

37

NSTextFields 上的键值观察

在您的-awakeFromNib方法的实现中,您编写了

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

这不会像您希望的那样做:self.fieldA不符合键的键值编码numA:如果您尝试发送-valueForKey:-setValue:forKey:使用键@"numA"self.fieldA,您将收到以下异常:

[ valueForUndefinedKey:]:此类对于键 numA 不符合键值编码。

[ setValue:forUndefinedKey:]:此类对于键 numA 不符合键值编码。

因此NSTextField实例也不符合键值观察@"numA":对于某些键,KVO 兼容的第一个要求是该键的 KVC 兼容。

但是,它与 KVO 兼容,除其他外,stringValue. 这允许您执行我之前描述的操作。

注意:这一切都不会因您在 Interface Builder 中设置绑定的方式而改变。稍后再谈。

NSTextField 的 stringValue 上的 Key-Value 观察问题

NSTextField在 .上调用@"stringValue"时观察. 这是 KVO 内部结构的结果。-setStringValue:NSTextField

简要介绍 KVO 内部结构

当你第一次开始观察一个键值对观察一个对象时,对象的类就改变了——它的isa指针也改变了。您可以通过覆盖看到这种情况-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));
}

通常,类的名称从Object变为NSKVONotifying_Object

如果我们使用键路径调用-addObserver:forKeyPath:options:context:Objectwith 的@"property"实例Object——一个符合 KVC 的键——当我们接下来调用-setProperty:我们的实例Object(实际上,现在是 的实例NSKVONotifying_Object)时,以下消息将是发送到对象

  1. -willChangeValueForKey:通过@"property"
  2. -setProperty:通过@"property"
  3. -didChangeValueForKey:通过@"property"

打破这些方法中的任何一个都表明它们是从未记录的函数调用的_NSSetObjectValueAndNotify

所有这一切的相关性在于,该方法-observeValueForKeyPath:ofObject:change:context:在我们添加到我们的实例的观察者上调用,以获取来自Object的关键路径。这是堆栈跟踪的顶部:@"property" -didChangeValueForKey:

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

这与NSTextField和 有什么关系@"stringValue"

在您之前的设置中,您将观察者添加到您的文本字段中-awakeFromNib。这意味着您的文本字段已经是NSKVONotifying_NSTextField.

然后,您将按下一个或另一个按钮,该按钮将调用-setStringValue您的文本字段。您能够观察到这种变化,因为 -- 作为NSKVONotifying_NSTextField--your text field 的一个实例,在收到setStringValue:value实际收到的

  1. willChangeValueForKey:@"stringValue"
  2. setStringValue:value
  3. didChangeValueForKey:@"stringValue"

如上所述,从 内部didChangeValueForKey:@"stringValue",所有观察文本字段值的对象都@"stringValue"被通知该键的值在它们自己的实现中已更改-observeValueForKeyPath:ofObject:change:context:。特别是,对于您添加为 中文本字段的观察者的对象,情况更是如此-awakeFromNib

总之,您能够观察到文本字段值的变化,@"stringValue"因为您将自己添加为该键的文本字段的观察者,并且因为-setStringValue在文本字段上被调用

所以有什么问题?

到目前为止,在讨论“NSTextFields 上的键值观察问题”的幌子下,我们实际上只是理解了开头的句子

NSTextField在 .上调用@"stringValue"时观察.-setStringValue:NSTextField

听起来很棒!所以有什么问题?

问题是,-setStringValue:当用户在文本字段中输入时,或者即使在用户结束编辑之后(例如,通过跳出文本字段),它也不会被调用。(此外,-willChangeValueForKey:-didChangeValueForKey:手动调用。如果是,我们的 KVO 会工作;但它不会。)这意味着虽然我们的 KVO on在文本字段上调用@"stringValue"时工作,但当用户自己输入时它不起作用文本。-setStringValue:

TL;DR@"stringValue" : 上的KVONSTextField不够好,因为它不适用于用户输入。

将 NSTextField 的值绑定到字符串

让我们尝试使用绑定。

初始设置

创建一个带有独立窗口控制器(我使用了创意名称WindowController)的示例项目,并带有 XIB。(这是我在 GitHub 上开始的项目。)在类扩展中WindowController.m添加了一个属性:stringA

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

在 Interface Builder 中,创建一个文本字段并打开 Bindings Inspector:

绑定检查器

在“值”标题下,展开“值”项:

NSControl 值绑定

“绑定到”复选框旁边的弹出按钮目前已选中“共享用户默认控制器”。我们想将文本字段的值绑定到我们的WindowController实例,所以选择“文件的所有者”。发生这种情况时,“Controller Key”字段将被清空,“Model Key Path”字段将更改为“self”。

将 NSControl 值绑定到文件的所有者

我们想将此文本字段的值绑定到我们WindowController实例的属性stringA,因此将“模型键路径”更改为self.stringA

将 NSTextField 的值绑定到文件所有者的属性 stringA

至此,我们完成了。(到目前为止在 GitHub 上的进展。)我们已经成功地将文本字段的值绑定到我们WindowController的属性stringA

测试它

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

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

在文本字段中显示“hello world”

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

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

回复 朦胧,再试一次

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

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

这看起来很棒。为什么我们一直没有谈论这个???这里有一个小问题:讨厌的按标签的东西。将文本字段的值绑定到字符串不会设置字符串值,直到在文本字段中结束编辑。

新希望

不过,希望还是有的!Cocoa Binding Documentation forNSTextField声明可用于 an 的一个绑定选项NSTextFieldNSContinuouslyUpdatesValueBindingOption. 你瞧,在 NSTextField 的值的绑定检查器中有一个对应于这个选项的复选框。继续并检查该框。

NSTextField 的值绑定不断更新 stringA

有了这个更改,当我们输入内容时,对窗口控制器stringA属性的更新会不断注销:

-[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>>

最后,我们不断地从文本字段中更新窗口控制器的字符串。其余的很容易。作为概念的快速证明,向窗口添加更多文本字段,将它们绑定到 stringA 并将它们设置为持续更新。此时你有三个同步NSTextField的s! 这是具有三个同步文本字段的项目。

剩下的路

您想要设置三个文本字段来显示彼此有某种关系的数字。由于我们现在正在处理数字,因此我们将删除该属性并将其替换stringA为,和:WindowControllernumberAnumberBnumberC

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

接下来,我们将第一个文本字段绑定到 File's Owner 上的 numberA,第二个文本字段绑定到 numberB,依此类推。最后,我们只需要添加一个属性,即以这些不同方式表示的数量。我们称之为 value quantity

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

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

我们需要恒定的转换因子来从单位转换为quantity单位numberA等等,所以添加

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 上项目的最终版本

于 2012-11-02T06:10:01.913 回答