NSView のレイアウト方式いろいろ
- Apple
- 開発
AppKit ってせっかく毎年改善されて使い方も変化しているのにほとんど "AppKit Release Notes" 以外の資料が更新されないから長年追い続けている人しか全体像が見えないのでないかといつも思っていたのだけど、WWDC18 ではついにリリースノートすらなくなってしまった! セッションビデオでの説明はあるものの過去の何本ものビデオを見返すなんて現実的ではないし、これ長い目で見るとよくないのでは...
そんなこともあるので zumuya では Cocoa 関係の情報をちょっとずつまとめるのである。今回はビューをレイアウトする方法について 1 つずつ触れていくことにする。
(2018.7.1:追記)
いつもよりだいぶ遅れてリリースノートが公開されました。
方式 1:手動で配置して autoresizingMask
で維持
昔ながらの方法。ある View がリサイズされるたび、その変化量に応じて subviews
各要素の frame
を決定するのだけど、そこで考慮されるのが autoresizingMask
だ。
autoresizingMask
はその名の通り複数指定できる。Interface Builder で指定するのが簡単でわかりやすい。内容は:
最初に理想的な親子の位置関係になるように手動で配置したうえでその関係を維持させるというのが基本の使い方。最近は Sketch のシンボル機能でも使われている方式。
例:上下左右にある 10 pt の余白が親 View をリサイズしても維持される。
シンプルな方式だけど欠点がある。一度でも superview
が小さくなりすぎて最初と同じ関係が維持できなくなると、その後いくら広げても崩壊してしまうのだ。“ステータスバーを表示”みたいなユーザの操作があると View を追加したり取り除いたりするたびに理想的な親子関係を作らないといけないし、昔はこの方法しかなかったから大変だった。
リサイズされるまでの流れにも触れておく:
- 何らかの要因でサイズが変更された
autoresizesSubview == true
なら次に進むresizeSubviews(withOldSize: NSSize)
が呼ばれるsubviews
各要素のresize(withOldSuperviewSize: NSSize)
が順番に呼ばれる- その中で
oldSuperviewSize
とautoresizingMask
をもとにリサイズされる
方式 2:constraints
を追加して自動で配置
「最近は Mac にも Auto Layout あるのね」みたいな話をときどき耳にするけど、導入されたのは OS X 10.7 Lion で iOS よりも先であることを強調しておく。それなりに大きな計算能力が必要だという背景もあったのかもしれないけれども。
ある View とある View(同じ場合もある)の座標属性の関係がこうであってほしいという情報、つまり制約を保持する NSConstraint
のインスタンスを複数作成しておいて View の constraints
に追加すれば、システムがそれぞれの関係を考慮して自動で配置してくれる賢い仕組み。
特に macOS では iOS と異なりユーザがウインドウをぐにぐにと好きなようにリサイズできるのが基本なので autoresizingMask
(方式 1)は簡単に崩壊してしまうし、環境設定ウインドウを見ればわかるようにリスト表示主体の iOS よりも複雑な配置をすることが多くてしかも各 View のサイズがローカライズに依存することも考えるとなると頭が痛いのだ。そんなときに重宝するのがこれ。ウインドウの最大サイズまで自動で決めてくれる優れものである。
これも Interface Builder で指定するのが視覚的にわかりやすくて楽なのだけど、方式 1 と同じ内容の例を示しておこう。
今回の例は親子だけど兄弟同士でも制約は指定可能だし、縦横比率や「〜以上/以下」みたいな指定もできるので、複数の制約を組み合わせればかなり複雑な条件での配置ができる。
注意点は、一つでも矛盾する制約が存在するだけで動かなくなってしまうことだ。上手く priority
(優先度)を指定して共存させる必要がある。それと translatesAutoresizingMaskIntoConstraints
が標準で true
なので false
にセットしておかないと意図しない制約が追加されることがあるから注意。
なかなかクセがあり慣れるまでは大変だけど、一度制約が完成してしまえばその後はシステムに面倒を見てもらえるので楽だ。
ちなみに constraints
の変更は NSView.updateConstraints()
や NSViewController.updateViewConstraints()
をオーバーライドしてその中でやるのが行儀がいいようで、それらは制約の変更が必要なときに needsUpdateConstraints = true
をすれば効率よく呼ばれる。
方式 3:layout()
をオーバーライドして手動で配置
手動で配置するという点では最初の方式に近いのだけど、それを View のリサイズのたびに呼ばれる layout()
の中でやる。単純にオーバーライドすればよい。UIKit の世界では昔からおなじみの方式だ。
毎回位置関係を計算しなおすから崩壊を防ぐことができるし、場面によっては方式 2 の制約を考えるよりも楽だったりする。UITableViewCell
みたいな効率的な使い回しなど、状況に応じて動的に View を追加/除去したい場合にも対応しやすい。
また同じ例で。
独自コントロールを作るときにはこの方式を使用することが多いかも。
ちなみに draw(_:)
に対する needsDisplay
みたいに layout()
に対する needsLayout
がある。再反映したくなったら直接 layout()
を呼ばずにそちらを呼ぶのが基本だ。
方式 4:NSStackView
や NSGridView
に配置させる
NSStackView
は UIStackView
の AppKit 版である。いやいやこれは Mac が先。とはいえ WPF(雑に言うと Windows における AppKit みたいなもの)にはもっと昔から StackView
も Grid
もある...
複数の View を一直線に等間隔で並べたいときには NSStackView
を使っておくと楽だ。自力で自動レイアウトの制約を作ってもいいのだけど、これなら一部の View を表示したり隠したりさせたときに自動で詰めてくれる。macOS 10.12 で追加された NSGridView
は名前の通りそのグリッド配置版だ。環境設定画面と相性がいいかも。
どちらも最近は Interface Builder で簡単に扱えるのでどんどん使いましょう。
方式 5:NSCollectionView
NSCollectionView
は UICollectionView
よりだいぶ前に登場したのだけど、当初は限られた機能しかなくてほとんど使われることもなく放置され、何年も後のアップデートで UICollectionView
そっくりの内容に変更されたという経緯を持つ。実はソースコードを UICollectionView
と共有しているのではないかという噂もある。
一言で表すと NSTableView
の二次元配置版みたいな立ち位置。データの個数分の View を用意していてはとても足りないような大量のデータを流し込んでスクロール表示したいときはこれ。選択状態も管理してくれる。
配置にこだわりたければ NSCollectionViewLayout
のサブクラスを作ればそれぞれの表示属性(NSCollectionViewLayoutAttributes
)をカスタマイズできるのだが UICollectionViewLayoutAttributes
みたいに transform3D
を指定することができないのが残念なところ。
Share
リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。