将KVO用于绑定在一起的NSTextField

wig*_*ing 8 cocoa objective-c key-value-observing nstextfield cocoa-bindings

我无法让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
Run Code Online (Sandbox Code Playgroud)

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

Nat*_*ler 36

NSTextFields上的键值观察

在你-awakeFromNib方法的实现中,你已经写过了

[self.fieldA addObserver:self 
              forKeyPath:@"numA" 
                 options:0 
                 context:&MainClassKVOContext];
Run Code Online (Sandbox Code Playgroud)

这不会做你希望它会:self.fieldA是不是键值编码兼容的关键numA:如果你尝试发送-valueForKey:-setValue:forKey:用钥匙@"numA"self.fieldA,你会得到以下情况除外:

[valueForUndefinedKey:]:此类不是密钥numA的密钥值编码兼容.

[setValue:forUndefinedKey:]:此类不是密钥numA的密钥值编码兼容.

其结果是,该NSTextField实例不是键值观察兼容@"numA",无论是:第一个要求是KVO兼容一些关键是要KVC兼容该键.

但是,除其他外,它符合KVO标准stringValue.这允许您执行我之前描述的操作.

注意:在Interface Builder中设置绑定的方式不会改变这一点.稍后会详细介绍.

在NSTextField的stringValue上使用键值观察的麻烦

在被召唤时观察作品NSTextField的价值.这是KVO内部的结果.@"stringValue"-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));
}
Run Code Online (Sandbox Code Playgroud)

通常,类的名称从更改ObjectNSKVONotifying_Object.

如果我们叫-addObserver:forKeyPath:options:context:上的一个实例Object与关键路径@"property"--a键,其中实例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:] ()
Run Code Online (Sandbox Code Playgroud)

请问这个涉及到NSTextField@"stringValue"

之前的设置中,您在文本字段中添加了一个观察者-awakeFromNib.这意味着您的文本字段已经是一个实例NSKVONotifying_NSTextField.

然后,您可以按一个或另一个按钮,而该按钮又会调用-setStringValue您的文本字段.您能够观察到此更改,因为 - 作为 - NSKVONotifying_NSTextField您的文本字段的实例,在收到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:未手动称为如果他们成功了,我们的志愿会的工作;但事实并非如此),这意味着,当我们的志愿在@"stringValue"工作时-setStringValue:被调用的文本字段,但它不是,当用户自己进入工作文本.

TL; DR:志愿上@"stringValue"NSTextField不够好,因为它没有对用户输入的工作.

将NSTextField的值绑定到字符串

让我们尝试使用绑定.

初始设置

创建一个示例项目,其中包含一个单独的窗口控制器(我使用了广告素材名称WindowController)和XIB.(这是我从GitHub开始的项目.)在类扩展中WindowController.m添加了一个属性stringA:

@interface WindowController ()
@property (nonatomic) NSString *stringA;
@end
Run Code Online (Sandbox Code Playgroud)

在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;
}
Run Code Online (Sandbox Code Playgroud)

在文本字段中显示

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

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

回复朦胧,再试一次

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

-[WindowController setStringA:]: stringA: <<(null)>> => <<some text>>
Run Code Online (Sandbox Code Playgroud)

这看起来很棒.为什么我们一直没有谈论这个?这里有点麻烦:讨厌的紧迫物.将文本字段的值绑定到字符串不会设置字符串值,直到编辑在文本字段中结束.

新希望

但是,仍然有希望!该可可绑定文件NSTextField规定,可为一个装订选项NSTextFieldNSContinuouslyUpdatesValueBindingOption.并且看,在Bindings Inspector中有一个对应于此选项的复选框,用于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>>
Run Code Online (Sandbox Code Playgroud)

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

其余的方式

您想要设置三个显示彼此具有某种关系的数字的文本字段.因为我们用数字现在正在处理,我们将删除该属性stringAWindowController,取而代之的是numberA,numberBnumberC:

@interface WindowController ()
@property (nonatomic) NSNumber *numberA;
@property (nonatomic) NSNumber *numberB;
@property (nonatomic) NSNumber *numberC;
@end
Run Code Online (Sandbox Code Playgroud)

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

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

@property (nonatomic) NSNumber *numberA;
@property (nonatomic) NSNumber *numberB;
@property (nonatomic) NSNumber *numberC;
@end
Run Code Online (Sandbox Code Playgroud)

我们需要不断的转换系数从单位转换quantity到的单位numberA等等,所以加

static float convertToA = 1000.0f;
static float convertToB = 573.0f;
static float convertToC = 720.0f;
Run Code Online (Sandbox Code Playgroud)

(当然,请使用与您的情况相关的数字.)有了这么多,我们可以为每个数字实现访问器:

- (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];
}
Run Code Online (Sandbox Code Playgroud)

所有不同数量的访问器现在只是间接访问机制quantity,非常适合绑定.还有一件事还有待完成:我们需要确保观察者在quantity改变时重新输出所有数字:

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

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

+ (NSSet *)keyPathsForValuesAffectingNumberC
{
    return [NSSet setWithObject:@"quantity"];
}
Run Code Online (Sandbox Code Playgroud)

现在,无论何时键入其中一个文本字段,其他文本字段都会相应更新. 这是GitHub上项目的最终版本.

  • 这是我见过的最全面的答案,我已经在这里待了很长时间.+1 (3认同)