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:
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
テキスト フィールドのインスタンスとして、受信時setStringValue:value
に実際に受信したためです。
willChangeValueForKey:@"stringValue"
setStringValue:value
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 で、テキスト フィールドを作成し、バインディング インスペクターを開きます。

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

[バインド先] チェックボックスの横にあるポップアップ ボタンでは、現在 [共有ユーザー デフォルト コントローラー] が選択されています。テキストフィールドの値を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
が設定されます。セッターをオーバーライドすることでこれを確認できます。
- (void)setStringA:(NSString *)stringA
{
NSLog(@"%s: stringA: <<%@>> => <<%@>>", __PRETTY_FUNCTION__, _stringA, stringA);
_stringA = stringA;
}
返信 もやもや、やり直してください
テキスト フィールドにテキストを入力してタブを押すと、出力されます。
-[WindowController setStringA:]: stringA: <<(null)>> => <<some text>>
これはすばらしく見えます。なんでずっと話してないの???ここでちょっとした問題があります。タブを押すというやっかいなことです。テキスト フィールドの値を文字列にバインドしても、テキスト フィールドで編集が終了するまで文字列値は設定されません。
新たな希望
しかし、まだ希望はあります!Cocoa Binding Documentation for にNSTextField
NSTextField
は、 で使用できるバインディング オプションの 1 つが であると記載されていますNSContinuouslyUpdatesValueBindingOption
。そして見よ、NSTextFieldの値のBindings Inspectorには、まさにこのオプションに対応するチェックボックスがあります。そのボックスにチェックを入れてください。

この変更により、何かを入力すると、ウィンドウ コントローラーの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
に置き換えます。numberA
numberB
numberC
@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 のプロジェクトの最終バージョンです。