ドキュメントベースアプリケーションでもう少し凝ったことをしたい

Xcodeで、新規プロジェクトの作成をすると、最低限のドキュメントベースのアプリケーションのスケルトンが作成される。
これで充分な事もあれば、もう少し凝ったことをしたいこともある。
じゃあ、そのもう少し凝ったことをしたい時にどうしたら良いのか?をまとめてみた。

カスタムアプリケーションクラスを作る

まず、最初に考えつくのは、アプリケーションの起動・終了時に独自の動作を加えたい。
ってことは、NSApplicationのサブクラスを作れば良さそうだから、Add New File...でNSApplicationのサブクラスなファイルを作成する。
NSApplicationのサブクラス化
では、やってみよう。
クラスの作成
次に、SupportingFilesグループの中に、[アプリケーション名]-info.plistというファイルがあるはずなので、それを開く。
すると、下の図の赤線の部分「principal class」がNSApplicationになっているはずなので、 これをさっき作ったファイル(クラス)名に書き換える
Info.plist
ちなみに、principalと言うのは、主要なとか先頭に立つと言う意味らしい
更に、これだけでは各種リソースにアクセスできないので、Mainmenu.xibを開いて、一番上の立方体(File's Owner)をクリック、Identity inspectorで、ここもNSApplicationになっているはずなので、自分が作ったクラス名にする。
ちなみに、File's ownerっていうのは、そのxibファイルを所有しているクラス(ファイル)っていう意味で、ここを任意のクラス名にすることで、そのクラスは暗黙の(一番上の立方体として)インスタンス化されるので、xib内のアイテムに自由にアクセスできる。ここ重要! では、検証。僕は各メソッドが呼ばれた順序を追跡するために、コンパイラーオプションのデバッグ時のみ -DTRACECALL で TRACECALL というプリプロセッサマクロを定義しておいて、
#ifdef TRACECALL
#define TRACEFUNC	NSLog(@"%@ : %@", NSStringFromSelector(_cmd), [self class]);
#else
#define TRACEFUNC
#endif
こういうマクロを定義している。あとは、例えばinitなんかで、
- (id) init {
TRACEFUNC
  self = [super init];
  if (self) {
      //    初期化内容
  }
  return self;
}// end - (id) init
と、メソッド名宣言の直後にTRACEFUNCと書いてあげれば、デバッグモードで実行時に
init : Charleston
の様に、そのメソッドが呼ばれたタイミングがログされる。
メソッド呼び出しのタイミング検証が不要になったら -DTRACECALL という定義を消せばDEBUGというマクロとは別に動作してくれるので、他のNSLogにも影響を与えないので、自分としては重宝している。
では、これを使って、いくつかメソッドを実装して追いかけてみよう。
思いつくのは、
位かな?では、実装して実行してみよう。
をや?結果は・・・
あれ?初期化しかされないぞ?
Charleston[26465:403] init : Charleston
Charleston[26465:403] awakeFromNib : Charleston
initawakeFromNibは呼ばれるけど、 applicationDidFinishLaunching:applicationWillTerminate:が呼ばれない。
これでは使い物にならないので、ちょっと考えてみると、applicationDidFinishLaunching:や、applicationWillTerminate:は、NSApplicationDelegateプロトコルで定義されたメソッドだ。じゃあ、NSApplicationから、先ほど自分の作ったクラスにdelegateをしてあげれば良いんじゃ無いか?
と、言うことで再び、Mainmenu.xibを開いて、インターフェースビルダーモードに入る。
そして、下図のように、Aマークのアイコンから、一番上の立方体へ右ドラッグで線を引き、指を離した所に現れるメニューのdelegateを選べば、delegateは完了だ!
dragdrag2
これで実行してみよう。
Charleston[27108:403] init : Charleston
Charleston[27108:403] awakeFromNib : Charleston
Charleston[27108:403] applicationDidFinishLaunching: : Charleston
Charleston[27108:403] applicationWillTerminate: : Charleston
今度はちゃんと、applicationDidFinishLaunching:や、applicationWillTerminate:が呼ばれたね。あとは、NSApplicationDelegateプロトコルを眺めながら、必要なメソッドを定義していけば、アプリケーションに独自の動作を追加できる。

次はウィンドウまわり

アプリケーションに独自の動作を追加するのは、上の方法で良さそうだ。
では、ドキュメントが管理するデータとGUIを切り離すには?NSWindowのサブクラスを作る?
いや、それより、Cocoaには、NSWindowControllerという便利なクラスがある。
これを介してウィンドウにアクセスし、ドキュメントとデータをやりとりするのがスマートそうだし、Xcodeが作ってくれるNSDocumentのサブクラスにも、カスタムウィンドウを作るなら、俺を消して、こっちのメソッドを定義しろって部分があるので、それに従ってみることにする。
サブクラスを作ろう
サブクラス(ファイル)の新規作成はNSApplicationのサブクラスを作ったところと同じなので、図は割愛する。
あとは、NSDocumentのサブクラスをGUIから切り離したいので、Document.xibをウィンドウ名.xibにリネームして、File's ownerを上で作った、NSWindowControllerのサブクラスにしてあげれば良い。
具体的には、例によって、ウィンドウ名.xibをInterfaceBuilderで選んで、その中の一番上の立方体(File's Ownerを)選ぶ、そしたら、右のインスペクターウィンドウで、下の図のように自分が作ったクラス名を指定してあげればオッケーだ
customwindowcontroller
委譲は大事よね
先ほどの失敗を繰り返さないために、Cocoa Browser Airで確認してみると、NSWindowにもNSWindowDelegateっていうプロトコルがあるので、xibファイルの中のウィンドウインスタンスから、File's Owner(これは自分で指定したWindowControllerになっているはず)にdelegateをしてあげる。
windowdelegate1windowdelegate2
こんな感じだ。
動かしてみる前に
早速動かしてみたいところだけど、ちょっと我慢して、Cocoa Browser Airで、NSApplicationDelegateNSWindowDelegateプロトコルをみながら、必要そうなメソッドをどんどん定義してみた。
ざっくり並べてみると
カスタムアプリケーションクラスには
カスタムウィンドコントローラークラスには
カスタムドキュメントクラスには
こんな感じだ。ただし、shouldCloseWindowController:delegate:shouldCloseSelector:contextInfo:は、使い方が解らない上に、何もしないと、ここで動きが止まってしまうので、今回は割愛した。
その結果は?
では、早速コンパイルして実行してみよう。すると、起動したアプリケーションを終了するまでの結果は
init : ASApplication
awakeFromNib : ASApplication
finishLaunching : ASApplication
applicationWillFinishLaunching: : ASApplication
init : Document
makeWindowControllers : Document
initWithWindow: : ASWindowController
windowWillLoad : ASWindowController
awakeFromNib : ASWindowController
windowDidLoad : ASWindowController
init : Document
makeWindowControllers : Document
initWithWindow: : ASWindowController
windowWillLoad : ASWindowController
awakeFromNib : ASWindowController
windowDidLoad : ASWindowController
windowShouldClose: : ASWindowController
windowWillClose: : ASWindowController
replyToApplicationShouldTerminate: : ASApplication
shouldTerminate : YES
terminate: : ASApplication
こんな感じになった。当たり前だけど、カスタムドキュメントは、xibの中から取り去ったので、awakeFromNibが呼ばれない。
あとは、この順番を見ながら、どこで何をすれば良いか考えて欲しい。
とりあえず、上のリストをもう一度、呼ばれなかったものに、横線を引いて並べてみると
カスタムアプリケーションクラスでは
カスタムウィンドコントローラークラスでは
カスタムドキュメントクラスでは
が、呼ばれたことになる。なんか釈然としないので、この辺はもう少し追いかけてみることにする。

釈然としないので

なんかすっきりしないので、もう一度、新規アプリケーションを2つ作って検証する。
で、結果は
両者に違いはありませんでした。とほほ。。。
でも、それが解っただけでも収穫と思うべきか。
ちなみに、トレースの結果は以下の通りでした。
test1[3804:403] init : Application
test1[3804:403] awakeFromNib : Application
test1[3804:403] finishLaunching : Application
test1[3804:403] applicationWillFinishLaunching: : Application
test1[3804:403] init : Document
test1[3804:403] makeWindowControllers : Document
test1[3804:403] initWithWindowNibName: : WindowController
test1[3804:403] initWithWindow: : WindowController
test1[3804:403] windowWillLoad : WindowController
test1[3804:403] awakeFromNib : WindowController
test1[3804:403] windowDidLoad : WindowController
test1[3804:403] applicationDidFinishLaunching: : Application
test1[3804:403] applicationWillBecomeActive: : Application
test1[3804:403] applicationDidBecomeActive: : Application
	// switch to finer & return
test1[3804:403] applicationWillBecomeActive: : Application
test1[3804:403] applicationDidBecomeActive: : Application
	// type command "n"
test1[3804:403] init : Document
test1[3804:403] makeWindowControllers : Document
test1[3804:403] initWithWindowNibName: : WindowController
test1[3804:403] initWithWindow: : WindowController
test1[3804:403] windowWillLoad : WindowController
test1[3804:403] awakeFromNib : WindowController
test1[3804:403] windowDidLoad : WindowController
	// click window 2 close button
test1[3804:403] windowShouldClose: : WindowController
test1[3804:403] windowWillClose: : WindowController
	// click window 1 close button
test1[3804:403] windowShouldClose: : WindowController
test1[3804:403] windowWillClose: : WindowController
	// type command "q"
test1[3804:403] terminate: : Application
test1[3804:403] applicationShouldTerminate: : Application
test1[3804:403] replyToApplicationShouldTerminate: : Application
test1[3804:403] applicationWillTerminate: : Application

結論として

上の結果から解ったことは、 と言うことで、アプリケーションスケルトンのカスタマイズの基礎は一応終わり。
カスタマイズしたアプリケーションの中身は、皆さん自身が好きなように作り込んで下さい。