将KVO用于绑定在一起的NSTextField

时间:2012-11-01 02:41:27

标签: objective-c cocoa cocoa-bindings key-value-observing nstextfield

我无法让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

以下是其中一个文本字段的绑定: enter image description here

1 个答案:

答案 0 :(得分:35)

NSTextFields上的键值观察

-awakeFromNib方法的实施中,您已撰写

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

这并不是您所希望的:self.fieldA不是 key-value coding compliant 代码为numA:如果您尝试使用密钥-valueForKey:-setValue:forKey:发送@"numA"self.fieldA,您将获得以下例外情况:

  

[valueForUndefinedKey:]:此类与密钥numA不符合密钥值。

  

[setValue:forUndefinedKey:]:此类与密钥numA不符合密钥值编码。

As a resultNSTextField个实例不是键值观察兼容 @"numA":第一个要求是符合KVO标准的某些密钥与该密钥的KVC兼容。

然而,除其他事项外,它符合KVO stringValue。这样您就可以what I described earlier

注意:这些都不会因您在Interface Builder中设置绑定的方式而改变。稍后会详细介绍。

NSTextField' stringValue

上的键值观察问题

NSTextField上调用@"stringValue"时,-setStringValue:的{​​{1}}值会有效。这是KVO内部的结果。

KVO内部的简短访问

当您开始观察第一次观察对象的键值时,对象的类被更改 - 其NSTextField指针被更改。你可以通过覆盖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 - @"property"的实例符合KVC标准的密钥 - 接下来我们在Object的实例上调用-setProperty:(事实上,现在是Object的实例),以下消息将被发送到对象

  1. NSKVONotifying_Object传递-willChangeValueForKey:
  2. @"property"传递-setProperty:
  3. @"property"传递-didChangeValueForKey:
  4. 在这些方法中的任何一个中断都表明它们是从未记录的函数@"property"中调用的。

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

    -didChangeValueForKey:

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

    有何关系?

    previous setup中,您在@"stringValue"上的文字字段中添加了观察者。这意味着您的文本字段已经是-awakeFromNib的实例。

    然后,您可以按一个或另一个按钮,而该按钮又会在您的文本字段上调用NSKVONotifying_NSTextField。您能够观察到此更改,因为 - 作为-setStringValue的实例 - 您的文本字段,在收到实际收到的NSKVONotifying_NSTextField

    1. setStringValue:value
    2. willChangeValueForKey:@"stringValue"
    3. setStringValue:value
    4. 如上所述,在didChangeValueForKey:@"stringValue"内,所有观察didChangeValueForKey:@"stringValue"文本字段值的对象都会收到通知,表示此密钥的值已在其自己的@"stringValue"实现中发生变化{1}}。特别是,您在-observeValueForKeyPath:ofObject:change:context:中作为文本字段的观察者添加的对象也是如此。

      总之,您可以观察-awakeFromNib的文本字段值的变化,因为您将自己添加为该键的文本字段的观察者,并且因为{{1在文本字段上调用。

      那么问题是什么?

      到目前为止,在讨论" NSTextFields上的键值观察问题"我们只是真正理解了开场句

        

      @"stringValue"上调用-setStringValue后,观察NSTextField的{​​{1}}值是有效的。

      这听起来很棒!那么问题是什么?

      问题是@"stringValue"在文本字段中没有被调用,因为用户正在键入它或者甚至在用户结束编辑之后(例如,通过跳出文本字段)。 (此外,-setStringValue:NSTextField不是手动调用的。如果是,我们的KVO会起作用;但它不会。)这意味着我们在-setStringValue:上的KVO工作了当在文本字段上调用-willChangeValueForKey:时,当用户自己输入文本时, NOT 工作。

      TL; DR -didChangeValueForKey:的{​​{1}}上的KVO不够好,因为它不适合用户输入。

      将NSTextField的值绑定到字符串

      让我们尝试使用绑定。

      初始设置

      创建一个示例项目,其中包含一个单独的窗口控制器(我已使用广告素材名称@"stringValue")与XIB一起完成。 (Here's the project I'm starting from on GitHub.)在-setStringValue:中,在类扩展中添加了属性@"stringValue"

      NSTextField

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

      Bindings Inspector

      在"价值"标题,扩展"价值"项:

      NSControl Value Binding

      " Bind to"旁边的弹出按钮;复选框目前有#34;共享用户默认值控制器"选择。我们希望将文本字段的值绑定到我们的WindowController实例。所以选择"文件的所有者"代替。当发生这种情况时,"控制器键"字段将被清空并且"模型关键路径"字段将更改为" self"。

      Binding NSControl Value to File's Owner

      我们希望将此文本字段的值绑定到我们的WindowController.m实例的属性stringA,因此请更改"模型关键路径"到@interface WindowController () @property (nonatomic) NSString *stringA; @end

      Binding NSTextField's Value to File's Owner's property stringA

      此时,我们已经完成了。 (Progress so far on GitHub.)我们已成功将文本字段的值绑定到我们的WindowController属性WindowController

      测试出来

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

      self.stringA

      Showing 'hello world' in Text Field

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

      stringA

      回复朦胧,再试一次

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

      stringA

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

      新希望

      然而,仍有希望! Cocoa Binding Documentation for NSTextField表示- (id)init { self = [super initWithWindowNibName:@"WindowController"]; if (self) { self.stringA = @"hello world"; } return self; } 可用的一个绑定选项为stringA。并且看,在Bindings Inspector中有一个与此选项对应的复选框,用于NSTextField的值。来吧,检查那个方框。

      NSTextField's value binding continuously updates stringA

      有了这个改变,当我们输入内容时,窗口控制器的- (void)setStringA:(NSString *)stringA { NSLog(@"%s: stringA: <<%@>> => <<%@>>", __PRETTY_FUNCTION__, _stringA, stringA); _stringA = stringA; } 属性的更新将被持续注销:

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

      最后,我们不断从文本字段更新窗口控制器的字符串。其余的很容易。作为概念的快速证明,在窗口中添加几个文本字段,将它们绑定到stringA并将它们设置为连续更新。此时您有三个同步NSTextField s! Here's the project with three synchronized text fields.

      其余的方式

      您想要设置三个显示彼此之间存在某种关系的数字的文本字段。由于我们现在正在处理数字,因此我们会从NSContinuouslyUpdatesValueBindingOption中移除属性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>> NSTextField和{{1} }:

      stringA

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

      numberA

      我们需要使用常量转换因子从numberB的单位转换为numberC的单位,依此类推,所以添加

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

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

      quantity

      所有不同数量的访问者现在只是访问@interface WindowController () @property (nonatomic) NSNumber *quantity; @property (nonatomic) NSNumber *numberA; @property (nonatomic) NSNumber *numberB; @property (nonatomic) NSNumber *numberC; @end 的间接机制,非常适合绑定。还有一件事还有待完成:我们需要确保观察员在quantity发生变化时重新收集所有数字:

      numberA

      现在,无论何时键入其中一个文本字段,其他字段都会相应更新。 Here's the final version of the project on GitHub