ドキュメントベースアプリケーションでもう少し凝ったことをしたい
Xcodeで、新規プロジェクトの作成をすると、最低限のドキュメントベースのアプリケーションのスケルトンが作成される。
これで充分な事もあれば、もう少し凝ったことをしたいこともある。
じゃあ、そのもう少し凝ったことをしたい時にどうしたら良いのか?をまとめてみた。
カスタムアプリケーションクラスを作る
まず、最初に考えつくのは、アプリケーションの起動・終了時に独自の動作を加えたい。
ってことは、NSApplicationのサブクラスを作れば良さそうだから、Add New File...でNSApplicationのサブクラスなファイルを作成する。
NSApplicationのサブクラス化
では、やってみよう。
次に、SupportingFilesグループの中に、[アプリケーション名]-info.plistというファイルがあるはずなので、それを開く。
すると、下の図の赤線の部分「principal class」が
NSApplicationになっているはずなので、
これをさっき作ったファイル(クラス)名に書き換える
ちなみに、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にも影響を与えないので、自分としては重宝している。
では、これを使って、いくつかメソッドを実装して追いかけてみよう。
思いつくのは、
- awakeFromNib
- applicationDidFinishLaunching:
- applicationWillTerminate:
位かな?では、実装して実行してみよう。
をや?結果は・・・
あれ?初期化しかされないぞ?
Charleston[26465:403] init : Charleston
Charleston[26465:403] awakeFromNib : Charleston
initと
awakeFromNibは呼ばれるけど、
applicationDidFinishLaunching:と
applicationWillTerminate:が呼ばれない。
これでは使い物にならないので、ちょっと考えてみると、
applicationDidFinishLaunching:や、
applicationWillTerminate:は、
NSApplicationDelegateプロトコルで定義されたメソッドだ。じゃあ、
NSApplicationから、先ほど自分の作ったクラスにdelegateをしてあげれば良いんじゃ無いか?
と、言うことで再び、Mainmenu.xibを開いて、インターフェースビルダーモードに入る。
そして、下図のように、Aマークのアイコンから、一番上の立方体へ右ドラッグで線を引き、指を離した所に現れるメニューのdelegateを選べば、delegateは完了だ!
→
これで実行してみよう。
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を)選ぶ、そしたら、右のインスペクターウィンドウで、下の図のように自分が作ったクラス名を指定してあげればオッケーだ
委譲は大事よね
先ほどの失敗を繰り返さないために、
Cocoa Browser Airで確認してみると、
NSWindowにも
NSWindowDelegateっていうプロトコルがあるので、xibファイルの中のウィンドウインスタンスから、File's Owner(これは自分で指定したWindowControllerになっているはず)にdelegateをしてあげる。
→
こんな感じだ。
動かしてみる前に
早速動かしてみたいところだけど、ちょっと我慢して、
Cocoa Browser Airで、
NSApplicationDelegateと
NSWindowDelegateプロトコルをみながら、必要そうなメソッドをどんどん定義してみた。
ざっくり並べてみると
カスタムアプリケーションクラスには
- awakeFromNib
- finishLaunching
- terminate:
- replyToApplicationShouldTerminate:
- applicationDidBecomeActive:
- applicationDidFinishLaunching:
- applicationOpenUntitledFile:
- applicationShouldOpenUntitledFile:
- applicationShouldTerminate:
- applicationShouldTerminateAfterLastWindowClosed:
- applicationWillBecomeActive:
- applicationWillFinishLaunching:
- applicationWillTerminate:
カスタムウィンドコントローラークラスには
- initWithWindow:
- awakeFromNib
- windowWillLoad
- windowDidLoad
- windowShouldClose:
- windowWillClose:
カスタムドキュメントクラスには
- init
- awakeFromNib
- makeWindowControllers
- windowControllerWillLoadNib:
- windowControllerDidLoadNib:
こんな感じだ。ただし、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が呼ばれない。
あとは、この順番を見ながら、どこで何をすれば良いか考えて欲しい。
とりあえず、上のリストをもう一度、呼ばれなかったものに、横線を引いて並べてみると
カスタムアプリケーションクラスでは
- awakeFromNib
- finishLaunching
- terminate:
- replyToApplicationShouldTerminate:
- applicationDidBecomeActive:
- applicationDidFinishLaunching:
- applicationOpenUntitledFile:
- applicationShouldOpenUntitledFile:
- applicationShouldTerminate:
- applicationShouldTerminateAfterLastWindowClosed:
- applicationWillBecomeActive:
- applicationWillFinishLaunching:
- applicationWillTerminate:
カスタムウィンドコントローラークラスでは
- initWithWindow:
- awakeFromNib
- windowWillLoad
- windowDidLoad
- windowShouldClose:
- windowWillClose:
カスタムドキュメントクラスでは
- init
- awakeFromNib
- makeWindowControllers
- windowControllerWillLoadNib:
- windowControllerDidLoadNib:
が、呼ばれたことになる。なんか釈然としないので、この辺はもう少し追いかけてみることにする。
釈然としないので
なんかすっきりしないので、もう一度、新規アプリケーションを2つ作って検証する。
- アプリケーションのひな形そのまま、Document.xibのFile's ownerをDocumentとしたアプリ
- アプリケーションのひな形から、Document.xibを削除し、新規にDocument.xibを作り、そのFile's ownerをWindowControllerとしたアプリ。
で、結果は
両者に違いはありませんでした。とほほ。。。
でも、それが解っただけでも収穫と思うべきか。
ちなみに、トレースの結果は以下の通りでした。
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
結論として
上の結果から解ったことは、
- カスタムウィンドウコントローラークラスを作るときは、windowNibNameをmakeWindowControllersに置き換える
- 複数のカスタムウィンドウコントローラークラスを使うときは、NSWindowControllerのサブクラス毎に.xibファイルを定義して、makeWindowControllersの中でドキュメントに登録する
- NSWindowControllerのサブクラスをインスタンス化した.xibのFile's ownerはどちらでも構わない。
- ただし、NSWindowControllerには、ownerというメソッドがあるので、これが自分を返すのは不思議なので、File's ownerはNSDocumentのサブクラスが望ましいのだろう
と言うことで、アプリケーションスケルトンのカスタマイズの基礎は一応終わり。
カスタマイズしたアプリケーションの中身は、皆さん自身が好きなように作り込んで下さい。