TilePuzzle
以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。
1 アプリケーション
まず、Xcode 上でビルドして実行してみます。
最初に起動したときは、一部が欠けた画像が表示されます。これだとパズルにならないので、「File」メニューから「Reshuffle Tiles(タイルを再シャッフル)」を選びます。そうすると、ランダムにピースが入れ替えられます。「Show Solution(解答を表示)」を選ぶと、最終目標の画像が表示されます。ウインドウ内をクリックすると、さっきの状態に戻ります。
操作は簡単で、白くなっている抜けの部分のとなりにあるピースをクリックすると、そのピースが白い部分に動きます。「Edit」メニューから「Undo(取り消し)」を選ぶと、直前の操作が取り消されます。続けて取り消しを行えますし、「Redo(やり直し)」メニューが有効になっているので、取り消しを取り消すこともできます。
2 ファイル構成
プロジェクトを構成しているファイルは、以下のようになっています。
「Models」内の TilePuzzle_DataModel.xcdatamodel は、モデルファイルです。
TilePuzzleAppDelegate.h と .m は、アプリケーションの委任であるコントローラーにあたる TilePuzzleAppDelegate クラスを定義しているファイルで、TilesView.h と .m は、表示を行うビュークラスである TilesView を定義しています。
「Resources」内の TigerPuzzle.png は、パズルに使われる(白抜きなしの)画像ファイルです。また、MainMenu.nib 内では、ウインドウなどのユーザーインターフェイスが定義されています。
「Other Frameworks」内に、CoreData.framework があることに注意してください。
3 モデルファイル
モデルファイルは、以下のようになっています。ここで矢印は、関連ではなく、継承関係です。すべてのエンティティのクラスは、NSManagedObject であり、独自クラスを実装しているわけではありません。
Tile の抽象属性がチェックされています。よって、このエンティティは実際には出現することはありません。残る 2 つの BlankTile と ImageTile は抽象ではなく、Tile を親として指定しているので、実際のデータファイル内には、この 2 つだけがエンティティとして存在することになります。Tile のプロパティは、correctXPosition(正しい X 位置)、correctXPosition(正しい Y 位置)、xPosition(X 位置)、yPosition(Y 位置)です。全部「整数 16」ですが、最小値が 0 で最大値が 3 になっていることに注意してください。タイトルは 4 × 4 に分割されていたので、これは実際の x、y 位置ではなく、タイル座標での位置です。また、すべてオプションでないので継承したエンティティのインスタンスは必ずこれらのプロパティがすべて設定されていることになります。デフォルト値も設定されていないので、必ず設定しなければなりません。また、一時でもないので、ファイル内に保存されることになります。これらのプロパティは、継承によって、残る 2 つのエンティティにも受けつがれることになります。
BlankTile は、独自に追加するプロパティを持っていません。Tile と同じですが、抽象ではなく、実際に使われるものであるところが違います。
ImageTile は、imageRectString という文字列プロパティを持っています。オプションでもなく一時でもありません。
Tile エンティティに対しては、受信要求テンプレートも定義されています。Tile エンティティを選択した状態で、右側の下向き三角メニューから「受信要求を表示」とします。すると、受信要求が前もって定義されているのがわかるでしょう。
まず、allTiles という名前で、述語 TRUEPREDICATE が定義されています。『述語プログラミングガイド』>「述語の書式文字列の文法」を見れば、「つねに TRUE として評価される述語です。」とあります。よってすべてのエンティティでつねに TRUE になり、すなわちこれを使えば全オブジェクトを取得できます。
つぎに、tileAtCorrectXAndY という名前で、述語 correctXPosition == $x AND correctYPosition == $y が定義されています。これはプロパティ correctXPosition が $x に等しく、correctYPosition が $y に等しいオブジェクトを受信するためのものです。この $x と $y は、使用時に代入されます。このような変数をもつ書式文字列に対しては、ここで使われている名前 x と y をキーとしてそれぞれの値を含むディクショナリを作り、管理対象オブジェクトモデルの fetchRequestFromTemplateWithName: substitutionVariables:(最初の引数が名前 tileAtCorrectXAndY、つぎが変数ディクショナリ)を使って、実際に使用する受信要求を取得できます。
最後に、tileAtXAndY という名前で、xPosition == $x AND yPosition == $y という述語が定義されています。これは上と同じですが、正しい位置ではなく、その時の位置でオブジェクトを探すものです。
こうした述語は、受信を行うときに一から作ることもできますが、同じ形式の受信をよく使うなら、モデルファイルに定義しておくことでもっと簡単に使うことができます。
4 MainMenu.nib
MainMenu.nib は次のようになっています。
 |
| MaiinMenu.nib(アウトレット) |
まず、TilePuzzleAppDelegate があり、これは File's Owner(アプリケーション)、ウインドウ、そしてウインドウ内の TilesView の delegate(委任)になっています。TilePuzzleAppDelegate 自体からは、window というアウトレットがウインドウに接続されています。
 |
| MaiinMenu.nib(アクション) |
アクションは、「File(ファイル)」メニューの「Reshuffle Tiles(タイルを再シャッフル)」から shuffle: が、「Show Solution(解答を表示)」から showSolution が、TilePuzzleAppDelegate をターゲットとして接続されています。
5 TilePuzzleAppDelegate クラス
さて、TilePuzzleAppDelegate クラスを見ていきますが、このアプリケーションの一部のインスタンス変数やメソッドは Core Data アプリケーションプロジェクトのテンプレートと似ています。つまり、Core Data アプリケーションプロジェクトを新規作成し、それを修正したもの、という風にもとらえることができます。そこで、ここでの説明では、まずテンプレートについて説明し、それに追加または修正されたものとして、このクラスを説明していくことにします。
5.1 インターフェイス宣言
まず、Core Data Application プロジェクトにおける AppDelegate クラスのインターフェイス宣言を見てみます。以下では、実装の説明も含めて、プロジェクト名と置換される部分については、XXX で置き換えてソースを示します。
XXX_AppDelegate.h > @interface インスタンス変数宣言
@interface XXX_AppDelegate : NSObject
{
IBOutlet NSWindow *window;
// ウインドウ
NSPersistentStoreCoordinator *persistentStoreCoordinator;
// 永続ストアコーディネーター
NSManagedObjectModel *managedObjectModel;
// 管理対象オブジェクトモデル
NSManagedObjectContext *managedObjectContext;
// 管理対象オブジェクトコンテキスト
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator;
// 永続ストアコーディネーターを返す
- (NSManagedObjectModel *)managedObjectModel;
// 管理対象オブジェクトモデルを返す
- (NSManagedObjectContext *)managedObjectContext;
// 管理対象オブジェクトコンテキストを返す
- (IBAction)saveAction:sender;
// 保存
@end
Core Data に特有のものとして、永続ストアコーディネーター、管理対象オブジェクトモデル、管理対象オブジェクトコンテキストを指すインスタンス変数があり、それを返すアクセサメソッドが宣言されています。これについては、個々のメソッドの実装を見れば理解できると思います。
Core Data とは別に、ウインドウに対するアウトレット、および保存動作を行うメソッドがあります。MainMenu.nib において、アウトレットはウインドウに接続されていて、saveAction: は「File(ファイル)」メニューの「Save」から、AppDelegate インスタンスをターゲットとして接続されています。
では、つぎに TilePuzzleAppDelegate クラスを見てみましょう。
TilePuzzleAppDelegate.h > @interface インスタンス変数宣言
@interface TilePuzzleAppDelegate : NSObject
{
IBOutlet NSWindow *window;
BOOL isShowingSolution;
// 解答を表示しているかどうか
NSManagedObjectModel *managedObjectModel;
NSManagedObjectContext *managedObjectContext;
NSImage *puzzleImage;
// パズル画像
}
- (NSManagedObjectModel *) managedObjectModel;
- (NSManagedObjectContext *) managedObjectContext;
- (IBAction) saveAction:(id)sender;
- (IBAction) shuffle:(id)sender;
// シャッフル
- (IBAction) showSolution:(id)sender;
// 解決を表示
- (void) createTiles:(NSManagedObjectContext *)context;
// タイル作成
- (void) swapTile:(NSManagedObject *)firstTile withTile:(NSManagedObject *)secondTile;
// タイル交換
@end
テンプレートと比べると、コメントを表示しているものだけが追加されていることがわかります。また、永続ストアコーディネーターに対する参照と、取得メソッドが除去されていることに注意してください。追加されたものについては、コメントでだいたい理解できると思います。詳細については、実装の説明を見てください。
5.2 Core Data の主要オブジェクトの取得
さて、インターフェイスを見てわかるように、Core Data 関連の 3 つのインスタンス変数と、それを取得するアクセサメソッドがテンプレートとこのサンプルで共通でした。まず、これを見てみましょう。ちなみにドキュメントベースの場合、このあたりの作業は、NSPersistentDocument がやってくれます。
まず、単純で独立している管理対象オブジェクトモデルの取得メソッドを見てみましょう。場所によって余分な空白は除去しています。
XXX_AppDelegate.m > managedObjectModel
/* アプリケーションバンドルとフレームワークバンドルすべてで見つかったモデルのすべてを結合することで
アプリケーションに対する管理オブジェクトを作成し、保持し、返す */
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
// すでにあるなら
return managedObjectModel;
// それを返す
}
// まだ設定されていなかったら
NSMutableSet *allBundles = [[NSMutableSet alloc] init];
// 空の変更可能集合を作成
[allBundles addObject: [NSBundle mainBundle]];
// アプリケーション主要バンドルを追加
[allBundles addObjectsFromArray:
[NSBundle allFrameworks]];
// 全フレームワークバンドルを追加
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:
[allBundles allObjects]] retain];
// 見つかったモデルを結合
[allBundles release];
// 集合はもう使わないので解放
return managedObjectModel;
}
まず最初にすでにインスタンス変数に設定されているかどうかを調べ、あるならそれを返し、ないなら以下に進みます。
つぎに、モデルを取得したいバンドルの集合を作成します。まず空の変更可能集合を作り、それにアプリケーションの主要バンドルを追加し、さらに全フレームワークバンドルを追加します。そして、NSManagedObjectModel のクラスメソッド mergedModelFromBundles: を使い、渡したバンドルに存在するすべてのモデルファイルを取得させ、結合しています。そして、取得に使った集合を解放しています。これでインスタンス変数 managedObjectModel が設定されました。
ここで取得しているモデルが、アプリケーションと全フレームワークのモデルであることに注意してください。また、管理対象モデルにはもっと違う取得法もあり、たとえば、モデルファイルの URL を指定して初期化できます。また、永続ストアコーディネーターに managedObjectModel を送ることで、そこで使われているモデルを取得することもできます(とはいえ、このサンプルでは永続ストアコーディネーターをモデルオブジェクトを使って初期化しているので、この場合意味がありませんが…)。
TilePuzzleAppDelegate クラスにおける、このメソッドは、上と全く同じです。テンプレートを修正せず、そのまま使っていることがわかります。
つぎに永続ストアコーディネーターを調べますが、その前に、そのメソッド内で使われているユーティリィティメソッドを見てます。
XXX_AppDelegate.m > applicationSupportFolder
/* Core Data ストアファイルを格納するために使われるアプリケーションに対するサポートフォルダを返す。
このコードは、内容に対して、NSApplicationSupportDirectory 位置、
または(それを見つけることができなければ)システムの一時ディレクトリ内のいずれかで、
「test」という名前のフォルダを使います。 */
- (NSString *)applicationSupportFolder {
NSArray *paths = NSSearchPathForDirectoriesInDomains
(NSApplicationSupportDirectory, NSUserDomainMask, YES);
// パスを検索
NSString *basePath = ([paths count] > 0)
// 見つかったら
? [paths objectAtIndex:0]
// 最初のもの
: NSTemporaryDirectory();
// なければ一時ディレクトリ
return [basePath stringByAppendingPathComponent:@"test"];
// test をパスに追加
}
NSSearchPathForDirectoriesInDomains は、Foundation の関数リファレンスで説明されています。ここでの引数は、順番に ApplicationSupport を探すこと、ユーザーフォルダ内で探すこと、チルダ(~)を拡張して通常のパス表現に変換すること、を指定しています。ここではユーザーフォルダ内ですが、もっと広いドメインで探すこともできるので、複数見つかることもあり、NSArray を返します。NSArray が空でなければ、basePath には見つかった最初のもの(ここでは、ユーザーフォルダ内のアプリケーションサポートフォルダ)を代入し、なければ一時ディレクトリを代入します。最後にそのパスに test を追加し、パスが見つかったフォルダの下位の「test」という名前のディレクトリを指すようにします。
さて、TilePuzzleAppDelegate クラスを見てみましょう。
TilePuzzleAppDelegate.m > applicationSupportFolder
// 保存されたデータが格納されるアプリケーションサポートフォルダを選択
- (NSString *) applicationSupportFolder {
NSString *applicationSupportFolder = nil;
NSArray *paths = NSSearchPathForDirectoriesInDomains
(NSApplicationSupportDirectory, NSUserDomainMask, YES);
if ( [paths count] == 0 ) {
NSRunAlertPanel
(@"Alert", @"Can't find application support folder", @"Quit", nil, nil);
[[NSApplication sharedApplication] terminate:self];
} else {
applicationSupportFolder
= [[paths objectAtIndex:0] stringByAppendingPathComponent:@"TilePuzzle"];
}
return applicationSupportFolder;
}
構造は、ほとんど同じです。ただし、見つからなかった場合、見つからないことを示すアラートパネルを「Quit(終了)」ボタンとともに表示し、その後、アプリケーションを終了しています。また、見つかった場合、最後に付ける文字列が「TilePuzzle」であることも違います。
さて、永続ストアコーディネーターの取得を見てみましょう。
XXX_AppDelegate.m > persistentStoreCoordinator
/* アプリケーションの永続ストアコーディネーターを返す。
この実装は、コーディネーターを作成して返し、アプリケーションにそれに格納させることになる。
(ストアに対するフォルダは、必要なら作成される。) */
- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
// あるなら
return persistentStoreCoordinator;
// それを返す
}
// ないなら
NSFileManager *fileManager;
NSString *applicationSupportFolder = nil;
NSURL *url;
NSError *error;
fileManager = [NSFileManager defaultManager];
// ファイルマネージャー取得
applicationSupportFolder = [self applicationSupportFolder];
// サポートフォルダ取得
if ( ![fileManager fileExistsAtPath:applicationSupportFolder isDirectory:NULL] )
// サポートフォルダがまだ存在していないなら
{
[fileManager createDirectoryAtPath:
applicationSupportFolder attributes:nil];
// 作成
}
url = [NSURL fileURLWithPath: [applicationSupportFolder
stringByAppendingPathComponent: @"test.xml"]];
// URL 作成
persistentStoreCoordinator
= [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:
[self managedObjectModel]];
// 管理対象オブジェクトモデルから初期化
if (![persistentStoreCoordinator addPersistentStoreWithType:NSXMLStoreType
configuration:nil URL:url options:nil error:&error]){
// ストアを追加して失敗したら
[[NSApplication sharedApplication] presentError:error];
// エラー提示
}
return persistentStoreCoordinator;
}
まず、すでに設定済みなら、それを返して終了します。
そうでないなら、ファイルマネージャーを取得し、サポートフォルダを取得し、フォルダがすでに存在するかどうかを調べます。なければ作成します。そして、そのフォルダ内の test.xml というファイルを指す URL を作成します。それから、上で説明したメソッドで管理対象オブジェクトモデルを取得して、それで永続ストアコーディネーターを初期化します。この永続ストアコーディネーターに、今作成した URL で示されるファイルを設定します。これが失敗したらエラーを提示します。成功しても失敗しても、ストアは返されることになります。
永続ストアコーディネーターは、永続ストアオブジェクトを管理して、それぞれがファイルに対応しています。したがって、複数のファイルを持っていて、それを結合して 1 つの出入口で管理できるようになっています。そのため、まずコーディネーターを作ってから、それにファイルから読み込まれたストアを追加するという流れになってることに注意してください。
TilePuzzleAppDelegate クラスには、永続ストアコーディネーターのインスタンス変数も取得メソッドも除去されていたので、続けて管理対象オブジェクトコンテキストの取得を見てみます。
XXX_AppDelegate.m > managedObjectContext
/* アプリケーションの管理対象オブジェクトコンテキストを返す
(これはアプリケーションの永続ストアコーディネーターにバインドされている)*/
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
// あるなら
return managedObjectContext;
// それを返す
}
NSPersistentStoreCoordinator *coordinator
= [self persistentStoreCoordinator];
// 永続ストアコーディネーター取得
if (coordinator != nil) {
// 取得できたら
managedObjectContext = [[NSManagedObjectContext alloc] init];
// 管理対象オブジェクトコンテキスト初期化
[managedObjectContext setPersistentStoreCoordinator: coordinator];
// 取得した永続ストアコーディネーターを設定
}
return managedObjectContext;
}
単純なので、コメントだけでわかると思います。まず、すでに設定されているかどうかを調べ、ないなら続きを行います。永続ストアコーディネーターを取得し、取得できたら、初期化した管理対象オブジェクトコンテキストに設定します。これだけです。
管理対象オブジェクトコンテキストは、アプリケーションにおける Core Data のデータ操作の中心です。通常、これを使います。もし、上の永続ストアコーディネーターの取得で、対応するファイルが存在していれば、そこからデータが読み込まれたはずです。そして、そのなかにあるデータから、管理対象オブジェクトコンテキストが再構成されます。では、最初にアプリケーションを動かして、まだ何のストアも存在しない場合はどうなるのでしょう。上の形では、このときは空の管理対象オブジェクトコンテキストが存在することになり、それに項目を追加していくことになります。最初に作成するときに何らかのデータを追加しておきたいなら、それを行わなければなりません。このサンプルでは、それが行われています。
TilePuzzleAppDelegate クラスには、永続ストアコーディネーターのメソッドがありませんでした。どうしているのでしょう。
TilePuzzleAppDelegate.m > managedObjectContext
// すでに作成されていなかったら、管理対象オブジェクトコンテキストと永続ストアコーディネーターを作成し、
// 管理対象オブジェクトコンテキストを返す
- (NSManagedObjectContext *) managedObjectContext {
NSError *error;
NSString *applicationSupportFolder = nil;
NSURL *url;
NSFileManager *fileManager;
NSPersistentStoreCoordinator *coordinator;
BOOL didCreateNewStoreFile = YES;
if (managedObjectContext) {
// 設定済みなら
return managedObjectContext;
// それを返す
}
// ないなら
fileManager = [NSFileManager defaultManager];
// ファイルマネージャー取得
applicationSupportFolder = [self applicationSupportFolder];
// サポートフォルダ取得
if ( ![fileManager fileExistsAtPath:applicationSupportFolder isDirectory:NULL] )
// サポートフォルダが存在しないなら
{
[fileManager createDirectoryAtPath:applicationSupportFolder attributes:nil];
// フォルダ作成
}
NSString *storeFilePath = [applicationSupportFolder
stringByAppendingPathComponent: @"TilePuzzle.xml"];
// 格納ファイルパス
if ([fileManager fileExistsAtPath: storeFilePath]) {
// すでにあるなら
didCreateNewStoreFile = NO;
// ファイル作成しない
}
url = [NSURL fileURLWithPath: storeFilePath];
// 格納パスの URL 作成
coordinator = [[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel: [self managedObjectModel]];
// 永続ストアコーディネーターをモデルから初期化
if ([coordinator addPersistentStoreWithType:NSXMLStoreType
configuration:nil URL:url options:nil error:&error]){
// コーディネーターにストア追加
managedObjectContext = [[NSManagedObjectContext alloc] init];
// 管理対象オブジェクトコンテキスト初期化
[managedObjectContext setPersistentStoreCoordinator: coordinator];
// コーディネーターを管理オブジェクトコンテキストに設定
} else {
[[NSApplication sharedApplication] presentError:error];
// ストア追加失敗したらエラー提示
}
[coordinator release];
// コーディネーター解放
// 既存のものを読み込むかわりに、新しいストアを作成するなら、
// タイルを表現する管理対象オブジェクトを作成する
if (didCreateNewStoreFile) {
//作成するなら
[self createTiles: managedObjectContext];
// タイルを作成
if (![managedObjectContext save:&error]) {
// 保存
[[NSApplication sharedApplication] presentError:error];
// 保存失敗ならエラー提示
[[NSApplication sharedApplication] terminate:self];
// アプリケーション終了
}
[[managedObjectContext undoManager] removeAllActions];
// 取り消しマネージャーを空に
}
return managedObjectContext;
}
大きな流れは、テンプレートの管理対象オブジェクトコンテキストの取得メソッドに、永続ストアコーディネーター取得のコードをはめこんだ形になっています。違うところは、データが格納されるファイルのパスを作った後で、そこにファイルが存在するかどうかチェックしていることです。存在しないなら、didCreateNewStoreFile が YES のままとなります。そして、最後で、管理対象オブジェクトの内容を作成して保存しています。取り消しマネージャーを空にしていますが、これは保存の後だからです。ここで呼ばれている createTiles: メソッドは、後で説明します。
5.3 テンプレートのメソッド
さて、主要オブジェクトの取得について調べてきました。テンプレートには、これ以外のメソッドも前もって実装されています。それらを調べてみましょう。まず、取り消しマネージャーを返すメソッドです。
XXX_AppDelegate.m > windowWillReturnUndoManager:
/* アプリケーションの NSUndoManager を返す
この場合、返されるマネージャーは、アプリケーションの管理対象オブジェクトコンテキストに対するもの */
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
return [[self managedObjectContext] undoManager];
}
単に、管理対象オブジェクトコンテキストの取り消しマネージャーを返しているだけです。このメソッドの実装は、TilePuzzleAppDelegate クラスでも全く同じです。
つぎは、保存を行っているメソッドです。これはテンプレートでは nib ファイルの「Save」メニューからアクションとしアプリケーションの委任に接続されていました。サンプルでは、保存メニュー項目がとりされているので、メニューからは呼ばれません。
XXX_AppDelegate.m > saveAction:
/* アプリケーションの保存アクションを実行
これは、アプリケーションの管理対象オブジェクトコンテキストに save: メッセージを送る
エラーに遭遇したら、ユーザーに提示される */
- (IBAction) saveAction:(id)sender {
NSError *error = nil;
if (![[self managedObjectContext] save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
これも単純です。単に管理対象オブジェクトコンテキストにエラー引数とともに save: を送り、失敗したら、戻ってきたエラーオブジェクトを提示しているだけです。このメソッドは、TilePuzzleAppDelegate クラス でも全く同じです。保存先は違いますが、どこに保存するかは永続ストアコーディネーターが管理しているので、このメソッドでは一切気にすることはなく、同じ実装が使えます。
つぎはアプリケーションの委任メソッドです。
XXX_AppDelegate.m > applicationShouldTerminate:
/* applicationShouldTerminate メソッドの実装
アプリケーションが終了する前に、アプリケーションの管理対象オブジェクトコンテキスト
における変更の保存を処理するために、ここで使われている */
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
NSError *error;
int reply = NSTerminateNow;
if (managedObjectContext != nil) {
// 管理対象オブジェクトコンテキストがあり
if ([managedObjectContext commitEditing]) {
// 未解決の編集を収容し
if ([managedObjectContext hasChanges]
&& ![managedObjectContext save:&error]) {
// 変更があるなら保存し、それが失敗したら
// このエラー処理は単純に「OK」ボタンをもつパネルにエラー情報を提示する
// エラー回復のための試み(つまり、エラーを修正する試み)を含まない
// その結果、実装はユーザーに情報を提示し、それから、変更を保存することなく
// ユーザーが Quit Anyway をユーザーが望むかどうかを問い合わせるパネルが続く
// 通常、この過程は、アプリケーション特有の回復手順を含めるよう変更したほうがいい
BOOL errorResult
= [[NSApplication sharedApplication] presentError:error];
if (errorResult == YES) {
reply = NSTerminateCancel;
// 終了をキャンセル
}
else {
int alertReturn = NSRunAlertPanel
// さらにアラート提示
(nil, @"Could not save changes while quitting.
Quit anyway?" , @"Quit anyway", @"Cancel", nil);
if (alertReturn == NSAlertAlternateReturn) {
reply = NSTerminateCancel;
}
}
}
}
else {
reply = NSTerminateCancel;
}
}
return reply;
}
まず管理対象オブジェクトコンテキストがあるかどうかチェックしています。つぎに未解決の編集があればそれを収容するように、commitEditing を送ります。これは収容に成功したら YES を返します。つぎに保存を行うので、その前に未解決があれば収容しておきます。そして、管理対象オブジェクトに保存していない変更があるか問い合わせて、あれば保存を行います。否定演算子が前につけられているので、失敗したら以下に進むことになります。
くわしいコメントが付けられているのでわかると思いますが、エラーアラートを最大二度提示しているだけです。
コメントでは、アプリューション特有のエラー処理をするように書かれていますが、TilePuzzleAppDelegate クラスでも全く同じです。
最後は dealloc メソッドです。
XXX_AppDelegate.m > dealloc
/* 保持した変数を解放するための、dealloc の実装 */
- (void) dealloc {
[managedObjectContext release], managedObjectContext = nil;
[persistentStoreCoordinator release], persistentStoreCoordinator = nil;
[managedObjectModel release], managedObjectModel = nil;
[super dealloc];
}
単に、Core Data 関連のインスタンス変数を解放して、スーパークラスの実装を呼び出しているだけです。TilePuzzleAppDelegate クラスでは以下のようになっています。
TilePuzzleAppDelegate.m > dealloc
- (void) dealloc {
[puzzleImage release];
[super dealloc];
}
puzzleImage が解放されています。これについては、init メソッドの説明を見てください。Core Data 関連のインスタンス変数が解放されていませんが、これは解放忘れです。残されている 2 つについては、ここで解放したほうがいいでしょう。このインスタンスは、起動で作られ終了時になくなるので、それほど問題ではありませんが…。
これでテンプレートとの比較は終了です。このサンプルのかなりメソッドがテンプレートのままであることがわかったと思います。残りはサンプル独自のメソッドです。これを説明していきます。
5.4 初期化
今まで説明したメソッドは、終了と解放を除いて、どれもアプリケーションの進行とは直接関連していませんでした。他のメソッド内から何らかの目的で呼ばれるメソッドです。このクラスは nib ファイル内でインスタンス化されているため、アプリケーションが起動し MainMenu.nib が読み込まれたとき、インスタンスが作成されることになります。この時、初期化メソッドが呼ばれることになります。
TilePuzzleAppDelegate.m > init
- (id) init {
self = [super init];
if (self) {
isShowingSolution = NO;
// 解決表示はしていない
NSString *imagePath = [[NSBundle mainBundle]
pathForImageResource:kPuzzleImageName];
// 画像パスをバンドルから取得
if (imagePath == nil) {
// パスが取得できなかったら
NSRunAlertPanel(@"Alert",
[NSString stringWithFormat:@"Image file missing from main bundle: %@",
kPuzzleImageName], @"Quit", nil, nil);
// アラート表示
[[NSApplication sharedApplication] terminate:self];
// アプリケーション終了
}
puzzleImage = [[NSImage alloc] initWithContentsOfFile:imagePath];
// 画像取得
if (puzzleImage == nil) {
// 画像取得できないなら
NSRunAlertPanel(@"Alert",
[NSString stringWithFormat:@"Can't open image file: %@",
kPuzzleImageName], @"Quit", nil, nil);
// アラート表示
[[NSApplication sharedApplication] terminate:self];
// アプリケーション終了
}
}
return self;
}
スーパークラスの初期化の後で、解決表示を NO にし、バンドル内にある画像のパスを取得し、それをインスタンス変数に読み込んでいます。これで終了です。
このクラスの初期化関連のメソッドは、これだけです。アプリケーションを起動させた時に、パズルが表示されていました。それはどうやって作られていたのでしょう。nib ファイル内でウインドウ属性に「Visible at launch time(起動時に表示)」が設定されていました。そのため、アプリケーションの起動時にウインドウが表示されることになります。ウインドウが表示される時、そのなかに置かれたビューも表示されます。そこで、ビューの描画メソッドが呼び出されます。この描画イメソッドのなかで、描くタイルを委任から取得しています。タイルが初めて問い合わせられた時、管理対象オブジェクトコンテキストから取得することになり、先ほど見たように、Core Data 関連オブジェクトがつぎつぎに作られることになり、初めての起動の場合、画像をタイル化することもそこで行われることになります。これは、managedObjectContext メソッド内で createTiles: で行われていました。そこで作られたタイルをビューが受けとり、タイルが表示されることになります。
TilePuzzleAppDelegate.m > createTiles:
// 管理対象オブジェクトコンテキスト内にパズルタイルを表現する管理対象オブジェクトを追加する
- (void) createTiles:(NSManagedObjectContext *)context {
NSSize imageSize = [puzzleImage size];
// 画像全体のサイズ
float pieceWidth = imageSize.width / kTileGridSize;
// タイル1個の幅
float pieceHeight = imageSize.height / kTileGridSize;
// タイル1個の高さ
// (0,0)から(3,2)に15個の画像タイルを配置
int tileX, tileY;
int i = 1;
for (tileX = 0; tileX < kTileGridSize; ++tileX) {
for ( tileY = 0; tileY < kTileGridSize && i < kNumTiles; ++tileY && ++i) {
// この長方形はタイルを描画するときこのタイルが使うことになる puzzleImage の一部を表す
NSRect imageRect = NSMakeRect
(tileX * pieceWidth, tileY * pieceHeight, pieceWidth, pieceHeight);
// 現在操作中のタイルの長方形
NSManagedObject *newTile;
newTile = [NSEntityDescription
insertNewObjectForEntityForName:@"ImageTile"
inManagedObjectContext:context];
// 画像タイル用のエンティティ記述を取得
[newTile setValue:[NSNumber numberWithInt:tileX] forKey:@"xPosition"];
[newTile setValue:[NSNumber numberWithInt:tileX]
forKey:@"correctXPosition"];
[newTile setValue:[NSNumber numberWithInt:tileY] forKey:@"yPosition"];
[newTile setValue:[NSNumber numberWithInt:tileY]
forKey:@"correctYPosition"];
// タイル長方形をNSStringFromRect(後でNSRectFromString )を使い文字列として表現
[newTile setValue:NSStringFromRect(imageRect) forKey:@"imageRectString"];
}
}
// 空白タイルを(3,3)に配置
NSManagedObject *blankTile;
blankTile = [NSEntityDescription
insertNewObjectForEntityForName:@"BlankTile"
inManagedObjectContext:context];
// 空白タイル用のエンティティ記述を取得
[blankTile setValue:
[NSNumber numberWithInt:(kTileGridSize - 1)] forKey:@"xPosition"];
[blankTile setValue:
[NSNumber numberWithInt:(kTileGridSize - 1)] forKey:@"correctXPosition"];
[blankTile setValue:
[NSNumber numberWithInt:(kTileGridSize - 1)] forKey:@"yPosition"];
[blankTile setValue:
[NSNumber numberWithInt:(kTileGridSize - 1)] forKey:@"correctYPosition"];
// このセットアップの取り消し登録を避けるため、取り消しマネージャーに removeAllActions する
// 最初に挿入されたオブジェクトに取り消し登録を強制させるため管理対象オブジェクトコンテキストに
// processPendingChanges を呼び出し、それから removeAllActions を呼び出す
[context processPendingChanges];
// 変更を処理させる
[[context undoManager] removeAllActions];
// 取り消し登録をクリア
}
行っていることは単純です。まず画像全体のサイズを取得し、タイル1個分の幅と高さを計算します。それから、各行で各列に対して、タイル1個ずつに繰り返しを行います。最初に現在対象としている部分の長方形を計算します。次に繰り返しの変数をもとに、タイル座標での x 位置、y 位置を設定します。最初は正しい位置にあるので、正しい座標に対しても同じ数値が使われています。タイルの位置を示す長方形も保存します。つぎに最後の空白タイルに対して同じことを行い、最後に今行った登録が取り消し可能操作として残らないように取り消しマネージャーを消去しています。
新しい管理対象オブジェクトの作成で、NSEntityDescription のクラスメソッドが使われていることに注意してください。モデルで使われたエンティティ名を使って指定したコンテキスト内に新しい管理対象オブジェクトを挿入します。それから、そのオブジェクトに値が設定されています。
先ほど述べたように、このメソッドで作られたタイルを使って、ビューが描画されることになります。このクラスを調べるのを中断して、タイルを表示しているビューについて見てみましょう。
6 TilesView クラス
6.1 インターフェイス宣言
まずインターフェイスから見ていきます。
TilesView.h > @interface
@interface TilesView : NSView {
IBOutlet id delegate;
// nib で TilePuzzleAppDelegate に接続
}
- (id) fetchTileAtX:(int)xPosition andY:(int)yPosition;
// タイルを受信
- (id) fetchTileAtCorrectX:(int)xPosition andY:(int)yPosition;
//正しいタイルを受信
- (void) drawTileAtX:(int)xPosition andY:(int)yPosition inRect:(NSRect)rect;
// (X, Y) にタイルを描画
- (BOOL) isOpaque;
// 不透明であることを返す
- (id) executeTileFetch:(NSFetchRequest *)request;
// タイル受信実行
const extern int kTileGridSize;
// タイルの行・列の個数 = 4
const extern int kNumTiles;
// タイルの個数 = 16
@end
コメントでわかると思います。const 定数は、実装ファイルにあるもので、コメントのように定義されています。
6.2 初期化
このクラスは、init... メソッドを実装していません。よって、初期化は NSView のものが使われます。しかし、awakeFromNib が実装されているので、nib が読み込まれて、このクラスのインスタンスが作成された時に、これが呼ばれることになります。
TilesView.m > awakeFromNib
- (void)awakeFromNib {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didUndo:) name:nil
object:[[delegate managedObjectContext] undoManager]];
}
単に通知を受けとるよう登録しているだけです。通知は、TilePuzzleAppDelegate クラスの管理対象オブジェクトコンテキストの取り消しマネージャーが取り消しを実行したときに送られるものです。その時に、didUndo: が呼び出されることになります。このメソッドは簡単なので、ここで見てみましょう。
TilesView.m > didUndo:
- (void)didUndo:(NSNotification *)notification {
[self setNeedsDisplay:YES];
}
単に、再表示が必要であるように指示しているだけです。これによって描画メソッドが呼ばれ、取り消し後の管理対象オブジェクトコンテキスト内の内容によって、ビューのタイルが描かれることになります。
6.3 描画
さて、このクラスの中心となる描画メソッドについて見るみます。その前に、isOpaque が実装されていて YES を返すことに注意してください。これによってビューでおおわれる部分の背後の上位ビューの描画メソッドは呼ばれなくなります。
TilesView.m > drawRect:
- (void)drawRect:(NSRect)rect {
// 背景色でビューを塗りつぶす
[[NSColor whiteColor] set];
[NSBezierPath fillRect:rect];
// 白色で塗りつぶし
NSSize tileSize;
// タイル1個のサイズを設定
tileSize.height = rect.size.height / kTileGridSize;
tileSize.width = rect.size.width / kTileGridSize;
// 各タイルを適切な長方形に描画する
NSPoint tileOrigin = rect.origin;
// タイル原点を長方形の原点に
int x, y;
for (x = 0; x < kTileGridSize; ++x) {
// 左から右へ繰り返し
for (y = 0; y < kTileGridSize; ++y) {
// 下から上へ繰り返し
NSRect gridRect = NSMakeRect(tileOrigin.x, tileOrigin.y,
tileSize.width, tileSize.height);
// 現在の描画タイル長方形
NSRect tileRect = NSInsetRect(gridRect, 1, 1);
// 1単位内側に
[self drawTileAtX:x andY:y inRect:tileRect];
// (x, y) 位置のタイルを長方形内に描画
tileOrigin.y += tileSize.height;
// 次のタイル用に高さを調節
}
tileOrigin.x += tileSize.width;
// 次のタイル用に幅を調節
tileOrigin.y = rect.origin.y;
// 次の列の一番下の行の長方形へ
}
}
ここまでコメントを付けなくてもよかった…。なんとなく付けてしまったのでそのままにしてあります…。ようするに、背景を描いた後は、左から右へ、下から上へと 1 個ずつその場所にタイルを描いているだけです。次に、実際に 1 つずつタイルを描画しているメソッドを見てみましょう。
TilesView.m > drawTileAtX: andY: inRect:
- (void)drawTileAtX:(int)xPosition andY:(int)yPosition inRect:(NSRect)rect {
NSManagedObject *fetchedTile;
if ([[delegate valueForKey:@"isShowingSolution"] boolValue]) {
// 解答を表示するなら
fetchedTile = [self fetchTileAtCorrectX:xPosition andY:yPosition];
// 正しいタイルを取得
} else {
// 通常は
fetchedTile = [self fetchTileAtX:xPosition andY:yPosition];
// 現在のタイルを取得
}
NSAssert(fetchedTile != nil,
([NSString stringWithFormat:@"Tile fetch at %d, %d returned nil!",
xPosition, yPosition]));
// 受信に失敗したら位置を出力
if ([[[fetchedTile entity] name] isEqual:@"BlankTile"]) {
// 空白タイルに対しては描画は必要ない
return;
}
NSString *rectString = [fetchedTile valueForKey:@"imageRectString"];
// 長方形を取得
NSAssert(rectString != nil,
@"[fetchedTile valueForKey:@\"imageRectString\"] returned nil!");
// 失敗したら出力
NSRect imageRect = NSRectFromString(rectString);
NSImage *puzzleImage = [delegate valueForKey:@"puzzleImage"];
// 画像を取得
NSAssert(puzzleImage != nil,
@"[delegate valueForKey:@\"puzzleImage\"] returned nil!");
// 失敗したら出力
// 単にビューの上に元画像(puzzleImage)の適切な部分を合成する
// 「適切な部分」はタイルに格納されている長方形で決定される
[puzzleImage compositeToPoint:rect.origin
fromRect:imageRect operation:NSCompositeCopy];
}
コメントでほぼわかると思います。まず解答を表示かどうかで受信する管理対象オブジェクトを変えます。あとは取得した管理対象オブジェクトに対して描画に必要な値を問い合わせて、それを使って画像の一部を合成しているだけです。
実際のデータ受信を行っているメソッドを見てみます。
TilesView.m > fetchTileAtCorrectX: andY:
- (id)fetchTileAtCorrectX:(int)xPosition andY:(int)yPosition {
NSDictionary *substitutionVars
// 変数ディクショナリを作成
= [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:xPosition], @"x",
[NSNumber numberWithInt:yPosition], @"y", nil];
NSFetchRequest *request
// 既定義のテンプレートから受信要求作成
= [[delegate managedObjectModel]
fetchRequestFromTemplateWithName:@"tileAtCorrectXAndY"
substitutionVariables:substitutionVars];
NSAssert(request != nil,
@"fetchRequestFromTemplateWithName:\"tileAtCorrectXAndY\" returned nil!");
// 受信要求が作れなければ出力
return [self executeTileFetch:request];
// 実際に受信を行い結果を返す
}
とても簡単なメソッドです。テンプレートについては「モデルファイル」の項目で説明しました。ここで渡した変数ディクショナリによって、その (x, y) 位置に対する受信要求が作られることになります。通常の位置を取得するメソッドもほとんど同じです。受信要求を作るために使われる名前が違うだけです。
さて、上で実際に受信を行う時に使われているメソッドを見てみましょう。このような形式になっているのは、正しい位置と通常の位置での違いが、使う受信要求だけだからでしょう。
TilesView.m > executeTileFetch:
- (id)executeTileFetch:(NSFetchRequest *)request {
// NSFetchRequest を使って xPosition、yPosition にあるタイルを受信する
// 受信要求はモデルから取得。この受信要求は Xcode のデータモデリングツールを使って定義され
// 「Tile」エンティティの「受信要求」リストで見ることができる
NSManagedObjectContext *context = [delegate managedObjectContext];
NSAssert(context != nil,
@"The TilesView's delegate's managedObjectContext is nil!.");
NSError *fetchError;
NSArray *fetchedTiles
= [context executeFetchRequest:request error:&fetchError];
// 受信実行
if (fetchedTiles == nil) {
// 受信できなければ
[[NSApplication sharedApplication] presentError:fetchError];
// エラーを提示
[[NSApplication sharedApplication] terminate:self];
// 終了
}
NSAssert([fetchedTiles count] == 1,
// 複数タイル受信なら出力
([NSString stringWithFormat:@"Tile fetch returned %d results!",
[fetchedTiles count]]));
return [fetchedTiles objectAtIndex:0];
}
非常に簡単です。TilePuzzleAppDelegate クラスに管理対象オブジェクトコンテキストを要求して、それに対して渡された受信要求を使って受信を行い、結果を返します。残りのコードはエラー処理です。
6.4 ユーザーに対する応答
描画が理解できたので、このクラスが行っていることは、後はユーザーのアクションに応答することです。これはマウスボタンが押された時に呼び出される mouseDown: メソッドで行われています。サンプルを起動させてわかるように、空白のとなりのタイルをクリックすれば、そのタイルが空白を埋めるように移動します。
TilesView.m > mouseDown:
- (void)mouseDown:(NSEvent *)event {
// 解答を示しているなら、マウスダウンはパズルモードに切り替えて戻る
if ([[delegate valueForKey:@"isShowingSolution"] boolValue]) {
[delegate setValue:[NSNumber numberWithBool:NO] forKey:@"isShowingSolution"];
} else {
// ユーザーがクリックしたタイルを見つけ出す
float viewWidth = NSWidth([self bounds]);
// バウンズの幅
float viewHeight = NSHeight([self bounds]);
// バウンズの高さ
NSPoint locationInView = [event locationInWindow];
// ウインドウ座標でのクリック位置
locationInView = [self convertPoint:locationInView fromView:nil];
// ビュー座標に変換
int tileX = (int)(locationInView.x / viewWidth * kTileGridSize);
// タイルx計算
tileX = tileX > (kTileGridSize - 1) ? (kTileGridSize - 1) : tileX;
// 適正範囲に修正
int tileY = (int)(locationInView.y / viewHeight * kTileGridSize);
// タイルy計算
tileY = tileY > (kTileGridSize - 1) ? (kTileGridSize - 1) : tileY;
// 適正範囲に修正
NSManagedObject *blankTile = [self blankTile];
// 空白タイルを取得
int blankX = [[blankTile valueForKey:@"xPosition"] intValue];
// 空白タイルx
int blankY = [[blankTile valueForKey:@"yPosition"] intValue];
// 空白タイルy
// クリックが空白タイルのとなりで行われたかを確認するチェック
if (tileX == blankX) {
// xが一致すれば
int distance = (blankY - tileY);
// y距離を計算
if (distance != -1 && distance != 1) {
// y距離がとなりでない
NSBeep();
return;
}
} else if (tileY == blankY) {
// yが一致すれば
int distance = (blankX - tileX);
// x距離を計算
if (distance != -1 && distance != 1) {
// x距離がとなりでない
NSBeep();
return;
}
} else {
NSBeep();
return;
}
// となりなら
NSManagedObject *clickedTile
= [self fetchTileAtX:tileX andY:tileY];
// タイル受信
[delegate swapTile:clickedTile withTile:blankTile];
// 委任にタイル交替してもらう
}
// ビューをリフレッシュする
[self setNeedsDisplay:YES];
}
コメントだけでほとんどわかると思います。空白タイルを得るメソッドは次で、タイル交替メソッドは後で説明します。
TilesView.m >blankTile
// 空白タイルを返す
Return the blank tile
- (NSManagedObject *)blankTile
{
// この受信要求はXcodeのモデリングツールを使って管理対象オブジェクトモデル内で簡単に指定できる
// しかし、例として、ここでプログラム上でそれを構築する
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
// 空の受信要求を作成
NSEntityDescription *entity
= [[[delegate managedObjectModel] entitiesByName] objectForKey:@"BlankTile"];
// 空白タイルのエンティティ記述を取得
[request setEntity:entity];
//エンティティを設定
NSManagedObjectContext *context = [delegate managedObjectContext];
// 管理対象オブジェクトコンテキストを取得
NSAssert(context != nil,
// 失敗したら出力
@"The TilesView's delegate's managedObjectContext is nil!.");
NSArray *fetchedTiles
= [context executeFetchRequest:request error:nil];
// 受信実行
NSAssert([fetchedTiles count] == 1,
// 複数受信したら出力
([NSString stringWithFormat:
@"There should have been 1 blank tile, but there were %d!",
[fetchedTiles count]]));
return [fetchedTiles objectAtIndex:0];
}
コメントにあるように、モデル内で定義もできます。前の受信要求は、エンティティに関係なく、位置だけで検索していましたが、今回はエンティティで検索します。
これで、このクラスのメソッドはすべて見ました。つぎは、TilePuzzleAppDelegate クラスに戻って、残りのメソッドを見ていきます。
7 TilePuzzleAppDelegate クラスふたたび
7.1 タイル操作
さて、ビューのマウスダウンメソッド内で、タイルを交替するように委任であるこのクラスのメソッドが呼ばれていました。それを見てみます。
TilePuzzleAppDelegate.m > swapTile:
- (void) swapTile:(NSManagedObject *)firstTile withTile:(NSManagedObject *)secondTile {
NSNumber *firstX = [firstTile valueForKey:@"xPosition"];
// 最初のx
NSNumber * firstY = [firstTile valueForKey:@"yPosition"];
// 最初のy
NSManagedObjectContext *context = [self managedObjectContext];
NSAssert(context != nil, @"managedObjectContext is nil!.");
[[context undoManager] beginUndoGrouping];
// 取り消しグループ開始
// 以下で位置交換
[firstTile setValue:[secondTile valueForKey:@"xPosition"] forKey:@"xPosition"];
[firstTile setValue:[secondTile valueForKey:@"yPosition"] forKey:@"yPosition"];
[secondTile setValue:firstX forKey:@"xPosition"];
[secondTile setValue:firstY forKey:@"yPosition"];
[[context undoManager] endUndoGrouping];
// 取り消しグループ終了
}
これも簡単です。単に2つの管理対象オブシェクトの値を交替しているだけです。ただし、操作を行う前に取り消しグループを作っています。これはユーザーが取り消しを選んだときに、交替時の個々の座標値の取り消しではなく、交替すべてを1回で取り消しできるようにするためです。
つぎにメニューから送られるアクションメソッド shuffle: を見てみます。これはパズルの位置をランダムに入れ替えてハズルとして機勤させるためのものです。
TilePuzzleAppDelegate.m > shuffle:
- (IBAction) shuffle:(id)sender {
// タイルの集まりを受信
NSFetchRequest *request
= [[self managedObjectModel] fetchRequestTemplateForName:@"allTiles"];
NSError *fetchError;
NSArray *fetchedTiles
= [[self managedObjectContext] executeFetchRequest:request error:&fetchError];
// 戻り値がないなら、エラー
if (fetchedTiles == nil) {
[[NSApplication sharedApplication] presentError:fetchError];
[[NSApplication sharedApplication] terminate:self];
}
// 予想したより少ないタイルしかなければアサート
NSAssert(([fetchedTiles count] == kNumTiles),
([NSString stringWithFormat:
@"There should be %d tiles, but there are only %u",
kNumTiles, [fetchedTiles count]]));
// 別のランダムなタイルと各タイルを交替することでタイルをシャッフル
// このサンプルコードは、交替位置によって配列内のタイルの単純なシャッフルを実行する
// しかし、これは結果の行列の逆転回数をカウントしていないため、解決不可能なパズルになる可能性がある
// このアプリケーションの実際のバージョンはこのチェックを行ったほうがいい
[[[self managedObjectContext] undoManager] beginUndoGrouping];
// 取り消しグループ開始
int i;
for (i = 0; i < kNumTiles; ++i) {
[self swapTile:[fetchedTiles objectAtIndex:i]
withTile:[fetchedTiles objectAtIndex:(random() % kNumTiles)]];
}
[[[self managedObjectContext] undoManager] endUndoGrouping];
// 取り消しグループ終了
[window display];
}
まず全タイルを取得します。それからエラー処理を行います。次にタイルの最初から始めて、ランダムに指定した番号のタイルと交替していきます。最後にシャッフルした結果を強制的に再表示させています。
つぎもメニューから呼ばれるアクションメソッドです。
TilePuzzleAppDelegate.m > showSolution:
- (IBAction) showSolution:(id)sender {
isShowingSolution = YES;
[window display];
}
解答を表示するアクションメソッドですが、行っていることは簡単です。描画メソッド内で isShowingSolution を調べて、解答表示ならそれにしたがった表示を行っていました。そのため、ここで行うことはこの変数を変更して再表示させることだけです。
7.2 残る委任メソッド
このクラスはほかにもウインドウとアプリケーションの委任メソッドをいくつか実装しています。windowWillClose: はウインドウの委任メソッドで、ウインドウが閉じられる前に呼び出されます。
TilePuzzleAppDelegate.m > windowWillClose:
- (void)windowWillClose:(NSNotification *)aNotification {
[self saveAction: self];
}
単に現在の状態を保存させているだけです。
つぎは開いている最後のウインドウが閉じられたときに呼ばれる委任メソッドです。
TilePuzzleAppDelegate.m > applicationShouldTerminateAfterLastWindowClosed:
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
return YES;
}
単に YES を返しているだけです。これにより最後のウインドウが閉じられた時、アプリケーションも終了します。このサンプルではウインドウは1つなので、それが閉じられた時、終了されることになります。これで NO を返せば、メインイベントループに戻ります。「New」などの項目があり、新規パズルゲームを始められるなら、それでもいいでしょう。
8 まとめ
これで、このサンプルの実装はわかりました。単純な例なので、これで全体が十分わかると思います。
ここで見たように、プロジェクトテンプレートでモデルを作成し、それを修正することで Core Data を簡単に使えます。独自の書類を扱うなら、ドキュメントベースを使えば、Core Data の主要オブジェクト関連の操作は自動でやってくれるので、さらに簡単になります。さらに、モデルから Interface Builder にドラッグ&ドロップして、それを修正し、バインディング中心でアプリケーションを作れば、もっと簡単になります。Core Data は簡単な永続化の機能とは別に、バインディングなどと組み合わせて使えば、コーディングを最小にして高機勤なアプリケーションを作ることが可能です。これについては他のサンプルを見てください。
管理人:神吉 秀典 E-mail: