ひ to り go と

Yosemite の NSViewController はこれまでとは違う!

MacDev

iOS App を作っている人なら、UIViewController が必要不可欠な存在であることがわかるはず。View(ビューとその子孫ビューのいくつか)を管理して、Model との関係をつなぎ、そのまとまり全体の処理を統括する Controller であり UIKit における MVC の柱と言える。

Mac ではどうだろうか。もともと AppKit では、1 つの NSWindowController ごとに MVC の関係を形成していた。View はウインドウであり、それに含まれる子孫ビューも含む。

ウインドウ単位で 1 Controller しか用意していないようではそのクラスがあまりに巨大化してしまう懸念があるけど、OS X 初期では処理ごとにウインドウが独立していることが多かったので、当時はそこまで不便ではなかった。ドロワーもシートもパネルもみんな独立したウインドウである。

OS X が進化するにつれ、Mac アプリケーションでは 1 つのウインドウ内にすべての機能を詰め込むようなデザインのものが多くなってきた。ドロワーは使われなくなったし、パネルで用意していたような機能もサイドバーとしてウインドウ内に配置されるようになってきた。こうなるともっと処理を分散させたい。そこで、OS X 10.5 Leopard において追加されたのが NSViewController というクラス。待望の、ビュー単位の Controller。これでアプリケーション開発が楽になるかと思いきや、本当に使い物にならなかった。

中身がなさすぎた

Leopard の時点で使い物にならないのはしかたがないとして、4 回もバージョンを重ねた OS X 10.9 Mavericks になっても NSViewController の中身はほとんど変化せず

  • -initWithNibName:bundle:
  • -loadView
  • .representedObject
  • .nibBundle
  • .nibName
  • .view
  • .title
  • -commitEditingWithDelegate:didCommitSelector:contextInfo:
  • -commitEditing
  • -discardEditing

たったこれだけ。ほとんど nib でビューを読み込んで包み込むだけ。ビューからの必要な通知も受け取れないし、レスポンダチェーンとしてアクションを受け取ることもできない。Cocoa のほかのクラスからも無視された状態であり、NSViewController とのつながりがあったのは OS X 10.7 Lion で追加された NSPopover と OS X 10.8 Mountain Lion で追加された NSPageController ぐらいだった。

この状況は OS X 10.10 Yosemite の登場で大きく変わることになる。手を入れるのが遅すぎると言いたくなるけど、今回は本気のようだ。便利なプロパティやメソッドが増えただけではなく、Cocoa アプリケーションを構成する一員としてしっかりと組み込まれた。今回は AppKit Release Notes for OS X v10.10 を読みながら、その変更点を紹介してみようと思う。

とは言っても、iOS の UIViewController とほぼ同等のものになったというだけの話である。

変更 1:ビューが読み込まれたときに呼ばれるメソッドが追加された。

なんで -[NSWindowController windowDidLoad] に相当するメソッドがないんだ!と誰もが思っていたはず。-awakeFromNib は意図しないタイミングで呼ばれたりするし、これからは -viewDidLoad を使いましょう。

変更 2:レスポンダチェーンに差し込まれるようになった。

Mac アプリケーション作ってる人ならこれを読むだけで「やっと楽になる」と思うはず。これがないからメニュー項目を選択したときのアクションすら実装できなかった。しかたなく自力でレスポンダチェーンに突っ込んでいた人も多いはず。

差し込まれる位置(アクションの伝達順序)は、ビューの階層が [ A [ B [ C ] ] ] だとすると
C → B → B のビューコントローラ → A → ウインドウ → ウインドウコントローラ

という順番。自身が管理するビューの次に回ってくる。

変更 3:ビューの表示状態が変化したときに呼ばれるメソッドが追加された。

  • -viewWillAppear
  • -viewDidAppear
  • -viewWillDisappear
  • -viewDidDisappear

UIViewController でおなじみ。UINavigationController がないから iOS ほど頻繁には使わないかも。

変更 4:ビューのレイアウト関係のメソッドが追加された。

  • -updateViewConstraints
  • -viewWillLayout
  • -viewDidLayout

NSView サブクラスを作らなくても動的なレイアウトが簡単にできるのは便利。制約を使った自動レイアウトでもいいし、自力で座標を計算して各サブビューの -setFrame: を呼ぶレイアウトも可能。

変更 5:コンテナ関係のメソッドが追加された。

  • -addChildViewController:
  • .childViewControllers
  • .parentViewController
  • -transitionFromViewController:toViewController:options:completionHandler:
  • -insertChildViewController:atIndex:
  • -removeChildViewControllerAtIndex:
  • -removeFromParentViewController
  • -addChildViewController:
  • -preferredContentSizeDidChangeForViewController:

コンテナという概念は、iOS 5 の UIViewController で追加されたものと同じ。ビュー同士が内包関係にあるのと同じように、ビューコントローラだってそんな関係の中に存在しているということ。

変更 6:インスタンスを初期化するときデフォルトでクラス名の nib をロードするようになった。

NSViewController サブクラスで nib を使う場合は、1 つのクラスに対して 1 つ nib を用意することがほとんどのはず。読み込む nib を指定するために -init をオーバーライドする必要がなくなったのだ。

変更 7:ウインドウの生成に使えるようになった。

  • NSWindow
    • +windowWithContentViewController:
    • .contentViewController
  • NSWindowController
    • .contentViewController

これまで特別な存在だったウインドウが、ポップオーバーと並ぶ「ビューコントローラのビューを表示するオブジェクト」の一つになったのだ。ウインドウコントローラの内容をシンプルにでき、ウインドウの表示内容を動的に置き換えたりするのも楽になる。

表示内容の部分だけ別クラスとして切り離されているから再利用もしやすい。例えばパネルとして表示している内容をサイドバーやポップオーバーとしても表示したい状況になっても問題ないのだ。

変更 8:ほかのビューコントローラを気軽に Presentation できるメソッドが追加された。

  • -presentViewController:animator:
  • -dismissViewController:
  • -presentViewController:asPopoverRelativeToRect:ofView:preferredEdge:behavior:
  • -presentViewControllerAsModalWindow:
  • -presentViewControllerAsSheet:
  • .presentedViewControllers
  • .presentingViewController

もうシートを表示するためにウインドウを作る必要はないし、NSPopover インスタンスを用意しなくてもポップオーバーを表示できてしまう。

何かを表示するときはどんなものでもまずビューコントローラを用意するように手順が統一されたのが気持ちいい。

変更 9:Storyboard に対応。

Interface Builder の GUI 環境でレイアウトした複数の nib ファイルをアプリケーションがバンドル内に保持して必要なタイミングで読み込んでインターフェイスを生成するのは、AppKit や Foundation と並ぶ NeXTSTEP から OS X に受け継がれた伝統であり、その後に登場する iOS の開発環境においても採用されていたのだけど、iOS 5 からは Storyboard と呼ばれる新しい仕組みが並行して使われるようになった。

従来は基本的に 1 つの表示状態ごとの nib ファイルを用意してそれらをコードでつないでいたのに対し、Storyboard では必要なビューをすべて一枚の巨大なキャンバス上に配置してそれらの遷移関係を GUI 上でつなぐ。

Yosemite では iOS と同様に Storyboard に対応したのだ。ここで変更 8 が活きてくる。GUI でビューコントローラ同士をつなぐだけで簡単にポップオーバーやシートを表示できてしまうのだ。Mac アプリケーションの作り方が根本的に変わるレベルの変更である。

変更 10:ビューコントローラのコンテナになるクラスが追加された。

  • NSSplitViewController
  • NSTabViewController

スプリットビューとタブビュー。従来は NSSplitViewNSTabView といったビュー(NSView のサブクラス)がその内容として複数のビューを管理していたのだけど、新しく追加されたのは NSViewController のサブクラスであり、管理する内容もビューではなくビューコントローラである。Storyboard 上で扱いやすいしコンテナの内包関係もはっきりするから便利。

...

OS X の 1 バージョンにおける NSViewController 関係の変更を書く記事だったのだけど、ほとんど UIViewController の機能全般を紹介するような内容になってしまった。これまでどれほど中身がなかったんだ。

とはいえ Apple の開発チームはいい仕事をしてる。iPhone のために 1 から作った UIKit とは異なり「歴史的経緯」が原因で嫌な設計が多く残る AppKit を、後付けでありながら上手いこと作り変えていると思う。AppKit とは別の、AppKit2 みたいなものを 0 から作ってほしいという気持ちはまだあるけれども...

Share

(参考になったらぜひ。記事を書くモチベーションの向上に役立てます。)