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:
了Object
with 的@"property"
实例Object
——一个符合 KVC 的键——当我们接下来调用-setProperty:
我们的实例Object
(实际上,现在是 的实例NSKVONotifying_Object
)时,以下消息将是发送到对象
-willChangeValueForKey:
通过@"property"
-setProperty:
通过@"property"
-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
实际收到的
willChangeValueForKey:@"stringValue"
setStringValue:value
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:
在“值”标题下,展开“值”项:
“绑定到”复选框旁边的弹出按钮目前已选中“共享用户默认控制器”。我们想将文本字段的值绑定到我们的WindowController
实例,所以选择“文件的所有者”。发生这种情况时,“Controller Key”字段将被清空,“Model Key Path”字段将更改为“self”。
我们想将此文本字段的值绑定到我们WindowController
实例的属性stringA
,因此将“模型键路径”更改为self.stringA
:
至此,我们完成了。(到目前为止在 GitHub 上的进展。)我们已经成功地将文本字段的值绑定到我们WindowController
的属性stringA
。
测试它
如果我们stringA
在 -init 中设置某个值,当窗口加载时,该值将显示在文本字段中:
- (id)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>>
这看起来很棒。为什么我们一直没有谈论这个???这里有一个小问题:讨厌的按标签的东西。将文本字段的值绑定到字符串不会设置字符串值,直到在文本字段中结束编辑。
新希望
不过,希望还是有的!Cocoa Binding Documentation forNSTextField
声明可用于 an 的一个绑定选项NSTextField
是NSContinuouslyUpdatesValueBindingOption
. 你瞧,在 NSTextField 的值的绑定检查器中有一个对应于这个选项的复选框。继续并检查该框。
有了这个更改,当我们输入内容时,对窗口控制器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
为,和:WindowController
numberA
numberB
numberC
@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 上项目的最终版本。