Cocoa Break Logo Top Soft Develop  脱力空間 Logo
Apple Web Badge
made by mi

概要 翻訳 ソース リンク がらくた

概要 Examples ADC Samples 3rd Parties etc CBOriginals

TemperatureConverter

以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。

目次:
1 README.rtf の日本語訳
2 アプリケーション
3 ファイル構成
4 MainMenu.nib
5 ApplicationDelegate クラス
5.1 継承とインスタンス変数
5.2 初期化
6 CentigradeValueTransformer クラス
6.1 インターフェイス
6.2 実装
7 他の値変換クラス
8 まとめ

1 README.rtf の日本語訳

異なる表現の間でデータを自動的に変換するために NSValueTransformer を使う単純なサンプルです。このサンプルの目的は、独自の NSValueTransformer のサブクラスを作成して使用する方法を示すことです。そうしないなら、より少ないコード行を使って、「通常の間に入るコード (regular glue code)」を使って、温度変換機を作成することは可能です。

Temperature Converter は、ユーザーが 4 つの異なる単位のどれかで温度値を入力できる単純なユーザーインターフェイスを提示し、アプリケーションは他の 3 つの単位における温度を自動的に変換し表示することになります。4 つの単位とは、ケルビン(絶対温度)(Kelvin)、摂氏 (Centigrade)、華氏 (Fahrenheit)(水の氷点を 0 度、沸点 を 212 度とする、摂氏 = (華氏 -32)* 5/9)、そしてそれほど頻繁には使われませんが、ランキン温度 (Rankine)(0 度が絶対温度 0 度、華氏 0 度が 459.67 度、摂氏 0 度が 491.67 度。)尺度です。

内部的には、アプリケーションは温度値をケルビンで格納し、入力された温度値をケルビンに変換するため、そして、ケルビンから必要とされる他の尺度に変換するために、値変換オブジェクト (value transformer) を使います。

クラス

アプリケーションは、ケルビンと他の 3 つの単位間で変換するために値変換オブジェクトクラスを使います。RankineValueTransformerCentigradeValueTransformer は、それぞれ、ケルビン単位とランキンと摂氏単位の間で変換を行います。

FahrenheitValueTransformer は、摂氏値から華氏温度尺度へと変換する前に、ケルビンから摂氏へ変換するために CentigradeValueTransformer を使います。これは、値変換オブジェクトがきわめて簡単に連鎖できることを実演するために設計されています。もうひとつの方法として、FahrenheitValueTransformerCentigradeValueTransformer のサブクラスとして作成して実装することもありえます。

3 つの値変換オブジェクトクラスは、逆変換を実装します。

ApplicationDelegate のインスタンスは、主要インターフェイスファイル内で見つけることができます。これは、アプリケーションの委任として動作し、NSValueTransformer によって提供されるメソッドによって、実行時に 3 つの値変換オブジェクトクラスを登録するために、-applicationWillFinishLaunching: メソッドを実装します。また、このメソッドは、アプリケーションが以前に一度も起動されたことがなければ、表示されるデフォルトの温度として、水の沸点を登録します。これは、ケルビンより摂氏においてたいてい最も自然に考えられるものなので、デフォルトの登録として、ユーザーデフォルト内にそれを入れる前に、摂氏からケルビンに水の沸点を直接変換するために CentigradeValueTransformer のインスタンスが使われます。

ApplicationDelegate は、RankineValueTransformer を使うように、Rankine 形式セルの値バインディングをプログラム上で設定します。結果のバインディングは、MainMenu.nib ファイル内で行われている、他のフィールドの値バインディングの設定と同じになります。

インターフェイス

アプリケーションは、温度値が入力でき、変換された値が表示されるように、4 つのフィールドを提示します。それぞれのフィールドは、Shared Defaults コントローラーの LastTemperature キーに接続された値バインディングを持っています。Shared Defaults コントローラーを使うことで、ユーザーによって入力された最後の値は、ユーザーデフォルトデータベースに格納されることになり、次回アプリケーションが起動された時に使われることになります。

Centigrade と Fahrenheit フィールドは、それぞれ、適切な値変換オブジェクトを使うように設定された値バインディングを持っています。Rankine フィルドの値バインディングは、ApplicationDelegate-awakeFromNib メソッド内でプログラム上で設定されます。

2 アプリケーション

まず、Xcode 上でビルドして実行してみます。アプリケーションを起動すると、次のようなウインドウが表示されます。

README.rtf の説明では、起動した直後に水の沸点が表示されているはずですが、10.4.11 の Xcode 2.5 では、何も表示されていない 4 つのフィールドがあるだけです。以前のバージョンでどうだったかを記憶してないのでわかりませんが。これについては、ApplicationDelegate の初期化で説明します。4 つのテキストフィールドのどれかに入力し、Enter または Return を押すか、他のフィールドに移れば、他のフィールドの値が入力された値にしたがって変更されます。

値を入力し変更して、それから終了すれば、ユーザーの「ライブラリ」>「Preferences」内に com.apple.examples.Temperature Converter.plist という名前の環境設定ファイルが作られているのがわかります。これを削除して、再びサンプルを起動し、今度は何も変更しないでそのまま終了させます。そうすると、このファイルは作られていません。このように環境設定システムは、デフォルト値以外の値が設定された場合に、ファイルを作成し、保存します。

3 ファイル構成

プロジェクトを構成しているファイルは、以下のようになります。

ApplicationDelegate.h と .m がコントローラークラスで、CentigradeValueTransformer.h と .m、FahrenheitValueTransformer.h と .m、RankineValueTransformer.h が値変換オブジェクトクラスになります。MainMenu.nib がアプリケーションの主要 nib ファイルとなります。

4 MainMenu.nib

MainMenu.nib は、次のようになっています。

MaiinMenu.nib 内のアウトレット接続

File's Owner(ファイル所有オブジェクト)の delegate(委任)に ApplicationDelegate が接続されています。また、ApplicationDelegate には 2 つのアウトレットがあり、rankineTemperatureField は Rankine: というタイトルがついたフィールドに、sharedUserDefaultsController は、Shared Defaults に接続されています。また、図では示していませんが、4 つのフィールドそれぞれ formatter というアウトレットがあり、nib ウインドウ内の Temperature Formatter に接続されています。このフォーマッターは、小数点以下 2 桁を表示するもので、最小値や最大値の設定はありません。

また、Rankine: 以外のテキストフィールドには value バインディングが設定されています。バインド先は Shared Defaults で、コントローラーキーは values です。モデルキーのパスは、LastTemperature です。Kelvin: フィールドには値変換は設定されていません。つまり、入力された値がそのまま反映され、逆もそうです。Centigrade: フィールドは centrigradeFromKelvin、Fahrenheit: フィールドは fahrenheitFromKelvin という値変換が設定されています。これによって、これら 3 つのフィールドに入力された値は、自動的に Shared Defaults に LastTemperature というキーのもとに値が保存されます。また、バインディングにより、他のフィールドの値もそれぞれ変換されます。nib 内だけでは、Rankine: フィールドがこの自動的な値設定からのけものにされていることになります。

5 ApplicationDelegate クラス

5.1 継承とインスタンス変数

ApplicationDelegate クラスを見ていきましょう。インターフェイス宣言は単純です。

ApplicationDelegate.h > 継承とインスタンス変数
@interface ApplicationDelegate : NSObject { IBOutlet NSFormCell *rankineTemperatureField; IBOutlet NSUserDefaultsController *sharedUserDefaultsController; } @end

MainMenu.nib 内で見たアウトレットがあるだけです。

5.2 初期化

このクラスで定義されているメソッドは、初期化に関するものだけです。独自の値変換オブジェクトの登録は、『値変換オブジェクトプログラミングガイド』>「値変換オブジェクトの登録」で説明されているように、クラスメソッド initialize のなかで行われています。

ApplicationDelegate.m > +initialize
+ (void) initialize { // NSValueTransformer に独自の値変換オブジェクトを登録 // これらの名前は IB 内のバインディングインスペクタの「Transformer」フィールドで使われる [NSValueTransformer setValueTransformer: [[CentigradeValueTransformer new] autorelease] forName: @"centrigradeFromKelvin"]; [NSValueTransformer setValueTransformer: [[FahrenheitValueTransformer new] autorelease] forName: @"fahrenheitFromKelvin"]; [NSValueTransformer setValueTransformer: [[RankineValueTransformer new] autorelease] forName: @"rankineFromKelvin"]; }

new メソッドは、NSObject クラスで定義されているもので、その動作は基本的には allocinit の組み合わせと同じです。ただ、異な動作を実装している場合もあることがリファレンスで説明されています。『値変換オブジェクトプログラミングガイド』>「値変換オブジェクトの登録」では、allocinit の組み合わせが使われています。この場合は、それほど気にすることはないでしょう。

setValueTransformer: forName:NSString を識別子として値変換オブシェクトを登録されます。以後は、この名前によって値変換が識別されることになります。

ApplicationDelegate で定義されている 3 つのメソッドのうち、このクラスメソッドは、クラスオブジェクトが初期化される時に呼び出されるので、一番先に呼ばれることになります。このクラスは、nib ファイル内でインスタンス化され、アプリケーションの委任にされていました。そのため、nib から復元される時の awakeFromNib と、アプリケーションの委任メソッド applicationWillFinishLaunching: を実装しています。このうち、awakeFromNib のほうは、nib から読み込まれた時に呼ばれるので、すべての準備がととのった後で呼ばれる applicationWillFinishLaunching: より先に呼ばれることになります。

こういう初期化関連など、メソッドの順番がよくわからない場合、ガイドやリファレンスを参照するのもいいですが、空メソッドや必要によってスーパークラスの実装を呼び出すだけのメソッドを定義して、ブレークポイントを設定し、デバッガでどういう順番で呼ばれるのかを確かめるのも有効です。このクラスの 3 つのメソッドの先頭にブレークポイントを設定し、デバッグすれば、どの順番で呼ばれているかを確認できます。

ApplicationDelegate.m > awakeFromNib
- (void) awakeFromNib { // ランキン温度値フィールドに対して値変換オブジェクトをプログラム上で登録 // 結果のバインディングは、IB 内で摂氏や華氏フィールドに対して設定したものと同じ // また、「NSValueTransformer」オプションを使うことで、 // 値変換オブジェクトの特定インスタンスにバインドすることも選択できる // これは、値変換オブジェクトが要求された変換を実行する値だけ以上の // 追加情報を必要とするような状況で使われる可能性がある NSDictionary *bindingOptions = [NSDictionary dictionaryWithObject: @"rankineFromKelvin" forKey: @"NSValueTransformerName"]; [rankineTemperatureField bind: @"value" toObject: sharedUserDefaultsController withKeyPath: @"values.LastTemperature" options: bindingOptions]; }

まず、値変換のバインディングオプションディクショナリを作成します。ここでは、値変換名に「rankineFromKelvin」という値という 1 組だけをもつディクショナリを作っています。そして、それを使って、sharedUserDefaultsController に対してバインディングを作成しています。

つぎに、アプリケーションの起動が終了する直前に applicationWillFinishLaunching: が呼ばれることになります。

ApplicationDelegate.m > applicationWillFinishLaunching:
- (void) applicationWillFinishLaunching:(NSNotification *)aNotification { // UI に現れることになる「最初の実行 (first run)」値として // 水の沸騰に対するケルビン温度を登録する // ケルビン温度は Shared Defaults コントローラーの「LastTemperature」キーの値として格納 // ユーザーが最後に入力したものは自動的に格納され、アプリの次の起動時に使われることになる // 値変換オブジェクトに対して何も特別に不思議なことはないことに注意 // 必要に応じて、コード内で値を変換するために自由にそれらを使う // ほとんどの人はケルビン値より 100.00 を水の沸点としてもっと容易に理解するので、 // 摂氏での水の沸点をケルビン値に変換するために、摂氏値変換オブジェクトが使われる NSValueTransformer *centrigradeFromKelvinTransformer = [NSValueTransformer valueTransformerForName:@"centrigradeFromKelvin"]; NSNumber *kelvinWaterBoilingPoint = [centrigradeFromKelvinTransformer reverseTransformedValue: [NSNumber numberWithFloat: 100.00]]; NSDictionary *registrationDefaults = [NSDictionary dictionaryWithObject: kelvinWaterBoilingPoint forKey: @"LastTemperature"]; [[NSUserDefaults standardUserDefaults] registerDefaults: registrationDefaults]; }

さて、先ほどの「2 アプリケーション」で説明したように、Mac OS X v10.4.11 の Xcode 2.5 で起動しても、ここでの設定は反映されず、すべてのフィールドが空のままです。『Cocoa のためのユーザデフォルトプログラミングトピック』を見てみると、「デフォルトの使用」>「NSRegistrationDomain におけるデフォルト値の設定」において、クラスの initialize メソッドで登録を行うべきであることが述べられています。そこで、この applicationWillFinishLaunching: 内のコードをコピーして、先ほどの initialize メソッドのコードの後にペーストしてみます。このメソッドを消去するか、コメントアウトすることを忘れないでください。そしてビルドと実行を行うと、今度は各フィールドにデフォルト値が表示された状態になっています。

このサンプルは、特にユーザーデフォルトコントローラーを介して、バインディングによって値を設定しているため、アプリケーション起動がほぼ終了した段階でデフォルト値を設定しても、それが反映されないのだと思われます。もしここで設定するなら、コントローラー経由で値を設定するのも可能かもしれませんが、同記事のなかの「ユーザデフォルトおよびバインディング」>「initialValues と NSUserDefaults の registerDefaults:」において、上の方法の代わりに setInitialValues: を使わないように注意されています。これはおそらくコントローラーの値を設定しても、デフォルト値として登録されるわけではないからでしょう。

これで ApplicationDelegate のメソッドは終了です。これでサンプルは起動完了し、ユーザーからの入力を待つ状態になります。値の入力は各フィールドで AppKit に組み込みの機能で行われ、コントローラー経由のバインディングによって、入力値の反映などは自動的に行われます。アプリケーションの委任が行うことは、他にはありません。つぎは、各値変換クラスを見ていきます。

6 CentigradeValueTransformer クラス

6.1 インターフェイス

CentigradeValueTransformer クラスのインターフェイスは以下のようになっています。

CentigradeValueTransformer.h > @interface
@interface CentigradeValueTransformer : NSValueTransformer + (Class)transformedValueClass; // 値変換時に返す値のクラスを返す + (BOOL)allowsReverseTransformation; // 逆変換が可能かどうか - (id)transformedValue:(id)value; // 値を変換する - (id)reverseTransformedValue:(id)value; // 値を逆変換する @end

コメントで説明するように、公開されているメソッドは非常に単純です。実装もこれらのメソッドだけです。

6.2 実装

まずクラスメソッドを見てみます。

CentigradeValueTransformer.m > +transformedValueClass
+ (Class)transformedValueClass { return [NSNumber class]; }

非常に簡単です。値変換のときに返すのが NSNumber クラスであることを返しているだけです。ここで NSObject のクラスメソッド +class を使って、クラスオブジェクトが返されていることに注意してください。

CentigradeValueTransformer.m > +allowsReverseTransformation
+ (BOOL)allowsReverseTransformation { return YES; }

これも簡単です。逆変換が可能であることを示す YES を返しています。ここで NO とすると、逆変換は行えないことを意味します。

つぎは、変換を実行するメソッドです。

CentigradeValueTransformer.m > transformedValue:
- (id)transformedValue:(id)value { if (value == nil) return nil; // 値がnilならすぐ終了 if (![value respondsToSelector: @selector(doubleValue)]) { // 値をとりだせないなら [NSException raise: NSInternalInconsistencyException format: @"Value does not respond to -doubleValue. No idea what to do. (Value is an instance of %@).", [value class]]; } float kelvinInputValue = [value doubleValue]; // NSString と NSNumber を処理 float centigradeOutputValue = kelvinInputValue - 273.15; // 値変換 return [NSNumber numberWithDouble: centigradeOutputValue]; }

まず渡された valuenil かどうかをチェックしています。そうなら nil を返してすぐに終了します。つぎに respondsToSelector: によって、渡された valuedoubleValue というメソッドに応答できるかどうかを調べています。if 文を抜けた後を見ればわかるように、このクラスは doubleValue に応答できるものなら、何でも受けとって変換できるということで、そこでのコメントは間違っています。

応答できない場合、NSInternalInconsistencyException という名前で例外を発生しています。これは、Foundation の定数リファレンスにあり、「内部的なアサーションが失敗し、呼び出し側のコード内で予期されない条件が暗示される時に起こる例外の名前です。」となっています。その後の format: で表示される書式文字列を渡し、その後の可変個引数(ここでは 1 つだけ)で渡された値のクラスオブジェクトを渡しています。これは「(Value is an instance of %@)」の「%@」の部分に挿入されます。挿入されるときにクラスオブジェクトはクラス名を返して、その文字列が結果として挿入されることになります。

doubleValue に応答できることがわかれば、その次でその値をとりだしています。そして結果の摂氏の出力値を計算しています。入力値はケルビン温度とみなされるので、摂氏 0 度は、絶対温度 273.15 度なので、その関係を使って計算が行われます。最後に計算した値を NSNumber 内に入れて返して終了です。この NSNumber は先ほどのクラスメソッドで返すクラスとして指定したものです。

つぎに逆変換を行うメソッドを見てみましょう。

CentigradeValueTransformer.m > reverseTransformedValue:
- (id)reverseTransformedValue:(id)value { if (value == nil) return nil; // 値がnilならすぐ終了 if (![value respondsToSelector: @selector(doubleValue)]) { // 値をとりだせないなら [NSException raise: NSInternalInconsistencyException format: @"Value does not respond to -doubleValue. No idea what to do. (Value is an instance of %@).", [value class]]; } float centigradeInputValue = [value doubleValue]; // NSString と NSNumber を処理 float kelvingOutputValue = centigradeInputValue + 273.15; // 値変換 return [NSNumber numberWithDouble: kelvingOutputValue]; }

通常の変換メソッドとほとんど同じです。違うのは、実際の逆変換を計算している部分で、マイナスがプラスになっているだけです。

7 他の値変換クラス

他の 2 つの値変換オブジェクトも、上で説明した CentigradeValueTransformer クラスと同じです。違うのは、実際の値を計算している部分だけです。

8 まとめ

独自の値変換を実装するのがとても簡単であることがわかったと思います。このサンプルのコードは、そのままコピーして名前をかえれば、独自の値変換を実装する際のひな型として使えるでしょう。

Mac OS X v10.5 以降の Xcode 3.0 を使っているなら、Examples に SimpleTemperatureConverter というサンプルが追加されています。そこでは値変換オブジェクトは使われず、単にアクセサメソッドを使って KVC と KVO により値変換を実装しています。値変換を使うのではなく、モデル自体にさまざまな温度で設定を行う機能をもたせています。このサンプルと比べてみるといいかもしれません。


管理人:神吉 秀典 E-mail:puer@ba.wakwak.com