8

Cocoa アプリでバインドされているテキスト フィールドを KVO で操作するのに問題があります。ボタンを使用して NSTextFields に文字列を設定するときにこれが機能するようになりましたが、バインディングでは機能しません。いつものように、スタック オーバーフローからのヘルプは大歓迎です。

私のコードの目的は次のとおりです。

  • 複数のテキスト フィールドを結合する

  • 数値が 1 つのフィールドに入力されると、他のフィールドが自動的に更新されます

  • テキストフィールドの変化を観察する

NSObject サブクラスである MainClass のコードは次のとおりです。

#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

テキスト フィールドの 1 つのバインドは次のとおりです。 ここに画像の説明を入力

4

1 に答える 1

37

NSTextField でのキー値の観察

メソッドの実装では、次のよう-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の の値を監視することが機能します。これは、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));
}

通常、クラスの名前は からObjectに変わりますNSKVONotifying_Object

キー パス( のインスタンスが KVC 準拠のキー)を使用して with の-addObserver:forKeyPath:options:context:インスタンスを呼び出した場合、次に のインスタンス(実際には のインスタンス) を呼び出すと、次のメッセージが表示されます。オブジェクトに送信Object@"property"Object-setProperty:ObjectNSKVONotifying_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テキスト フィールドのインスタンスとして、受信時setStringValue:valueに実際に受信したためです。

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

上記のように、 内からdidChangeValueForKey:@"stringValue"、 のテキスト フィールドの値を監視しているすべてのオブジェクトに@"stringValue"、このキーの値が の独自の実装で変更されたことが通知されます-observeValueForKeyPath:ofObject:change:context:。特に、これは でテキスト フィールドのオブザーバーとして追加したオブジェクトに当てはまります-awakeFromNib

要約すると、@"stringValue"そのキーのテキスト フィールドのオブザーバーとして自分自身を追加し、テキスト フィールドでが呼び出されたため-setStringValue、 のテキスト フィールドの値の変化を観察できました。

だから問題は何ですか?

これまでのところ、「NSTextField でキー値を監視する際のトラブル」の議論を装って、最初の文だけを実際に理解できました。

で が呼び出されたときに、NSTextFieldの の値を監視することが機能します。@"stringValue"-setStringValue:NSTextField

そして、それは素晴らしいですね!だから問題は何ですか?

問題は-setStringValue:、ユーザーがテキスト フィールドに入力している間、またはユーザーが編集を終了した後でも (テキスト フィールドからタブで移動するなどして)、テキスト フィールドで が呼び出されないことです。(さらに、-willChangeValueForKey:-didChangeValueForKey:は手動で呼び出されません。もしそうなら、KVO は機能しますが、機能しません。) これは、テキスト フィールドで が呼び出されたときに KVO が機能する一方でユーザー@"stringValue"自身が入力したときには機能しないことを意味します。文章。-setStringValue:

TL;DR : の KVO は@"stringValue"NSTextFieldユーザー入力に対して機能しないため、十分ではありません。

NSTextField の値を文字列にバインドする

バインディングを使ってみましょう。

初期設定

別のウィンドウ コントローラー (私はクリエイティブ名を使用しましたWindowController) を使用して、XIB を備えたサンプル プロジェクトを作成します。(これは、私が GitHub で開始しているプロジェクトです。 )クラス拡張にWindowController.mプロパティを追加しました。stringA

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

Interface Builder で、テキスト フィールドを作成し、バインディング インスペクターを開きます。

バインディング インスペクター

「値」ヘッダーの下で、「値」項目を展開します。

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が設定されます。セッターをオーバーライドすることでこれを確認できます。

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

返信 もやもや、やり直してください

テキスト フィールドにテキストを入力してタブを押すと、出力されます。

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

これはすばらしく見えます。なんでずっと話してないの???ここでちょっとした問題があります。タブを押すというやっかいなことです。テキスト フィールドの値を文字列にバインドしても、テキスト フィールドで編集が終了するまで文字列値は設定されません。

新たな希望

しかし、まだ希望はあります!Cocoa Binding Documentation for にNSTextFieldNSTextFieldは、 で使用できるバインディング オプションの 1 つが であると記載されていますNSContinuouslyUpdatesValueBindingOption。そして見よ、NSTextFieldの値のBindings Inspectorには、まさにこのオプションに対応するチェックボックスがあります。そのボックスにチェックを入れてください。

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 にバインドして、継続的に更新するように設定します。この時点で、同期された が 3 つありNSTextFieldます。 これは、同期された 3 つのテキスト フィールドを持つプロジェクトです。

残りの道

互いに何らかの関係がある数値を表示する 3 つのテキストフィールドをセットアップしたいと考えています。ここでは数値を扱っているため、stringAからプロパティを削除して, andWindowControllerに置き換えます。numberAnumberBnumberC

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

次に、最初のテキスト フィールドを File's Owner の numberA にバインドし、2 番目のテキスト フィールドを numberB にバインドします。最後に、これらのさまざまな方法で表される量であるプロパティを追加する必要があります。その値を と呼びましょう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];
}

さまざまな number アクセサーはすべて、 にアクセスするための間接的なメカニズムにすぎずquantity、バインディングに最適です。やらなければならないことが 1 つだけ残っています。オブザーバーquantityが が変更されるたびにすべての数値を再ポーリングするようにする必要があります。

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

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

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

これで、テキストフィールドの 1 つに入力するたびに、それに応じて他のフィールドが更新されます。 これが GitHub のプロジェクトの最終バージョンです

于 2012-11-02T06:10:01.913 に答える