iOSアプリでUIを作る場合Storyboardを使うのが普通なんだけれど、プログラムから制御できると非常に助かる。音の数が増えたり、新たな要素が追加された場合に対応しやすいロジックを組んでおきたい。旧バージョンはすべての雨粒を線で繋いだ結果Xcodeが地獄の様相を呈した。レイアウトもLandscapeだったが、iPhoneがどんどん縦に伸びたこともあり、Portraitに変更した。
iOSのレイアウトシステムには、「Manual Layout」、「Auto Resizing(Spring and Struts)」と呼ばれる従来の方式に加え、多様なサイズの画面レイアウトに対応するための「Auto Layout」が採用されている。今回はAuto Layoutを使っていく。名前から想像すると色々と自動でやってくれそうな期待をしてしまうのだけど、全然そんなことはなくて(というか期待と違う結果になって)つまづきまくってDropophoneの開発が遅れた。
制約によるレイアウト
Auto LayoutはCassowaryというレイアウトシステムから発展した。制約(Constraints)をベースに、オブジェクト同士の関係を記述するルールを組んでいく。JavaScriptにも移植され、ブラウザで動くGrid Style Sheetsというプロジェクトもある。
Auto Layoutを使うか使わないかはViewごとに選択できる。プロパティ名がわかりにくい気がするが、Auto Layoutを適用したい要素に対してtranslatesAutoresizingMaskIntoConstraintsをfalseにする。
buttonA.translatesAutoresizingMaskIntoConstraints = false
関係を記述する手法として、VFLという言語が採用されている。CSSで要素を配置する場合は基本的にマークアップが強く影響するので、「ボタンAの右に30px空けてボタンBを置く」ということをコードで表現しにくい。
VFLだとこう書ける。
"V:|-[buttonA]-|"
"V:|-[buttonB]-|"
"H:|-[buttonA]-30-[buttonB]-|"
分かりやすい。アスキーアートっぽいのも面白い。"|"はSuperview(親View)、"-"はスペーシング、"[]"にはViewを記述する。Viewは変数名とインスタンスをまとめてDictionaryで渡す。
H(水平方向)の指定だけでなくV(垂直方向)の指定も必要。ボタンが横に2つ並ぶ場合など、カラムを取り扱うには同じような内容を繰り返すことになる。
let views: Dictionary = Dictionary(dictionaryLiteral:("buttonA", buttonA),("buttonB", buttonB))
let horizontalConstraints: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-[buttonA]-30-[buttonB]-|", options: [], metrics: nil, views: views)
let verticalConstraintsA: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-[buttonA]-|", options: [], metrics: nil, views: views)
let verticalConstraintsB: [NSLayoutConstraint] = NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-[buttonB]-|", options: [], metrics: nil, views: views)
view.addConstraints(horizontalConstraints)
view.addConstraints(verticalConstraintsA)
view.addConstraints(verticalConstraintsB)
上のコードを実行した画面がこれ。
確かにボタンAとボタンBの間は30px空いているのだけど、AとBのサイズは違うし、どう考えても縦に長過ぎる。レイアウトを詰めるには、ボタンのサイズやマージンなどを細かく指定していく必要があるのだが、どの設定が足りてなくてこの結果になっているのかが分からず、かなりややこしい。
VFLが解釈される前にViewの親子関係などが明らかになっている必要があるので、addSubViewなどの記述の順番には気をつける。Viewの初期化と制約の追加は関数を分けておいた方が良い。
最初は便利と思ったVFLだったが、全然理想通りにレイアウトできず完全に行き詰まってしまった。何か良い方法はないものかと調べていたら、Storyboard上で設定するようにaddConstraintで一つずつ設定していく手法を見つけた。基準となる要素を決めて、それに対してどのくらい離すとかを決めていく。これを覚えたら、イメージと実行結果の差がなくなり、一気に開発が進んだ。
view.addConstraint(
NSLayoutConstraint(item: buttonA,
attribute: .Top, relatedBy: .Equal,
toItem: view, attribute: .Top,
multiplier: 1.0, constant: 10
)
)
view.addConstraint(
NSLayoutConstraint(item: buttonA,
attribute: .Left, relatedBy: .Equal,
toItem: view, attribute: .Left,
multiplier: 1.0, constant: 10
)
)
view.addConstraint(
NSLayoutConstraint(item: buttonB,
attribute: .Top, relatedBy: .Equal,
toItem: buttonA, attribute: .Top,
multiplier: 1.0, constant: 0
)
)
view.addConstraint(
NSLayoutConstraint(item: buttonB,
attribute: .Left, relatedBy: .Equal,
toItem: buttonA, attribute: .Right,
multiplier: 1.0, constant: 30
)
)
buttonAのTopを親ViewのTopから10px、Leftを親ViewのLeftから10pxと設定し、buttonBはTopをbuttonAのTopと合わせ、LeftをbuttonAのRightから30pxと設定している。こうするとbuttonAのTopやLeftを変更した際に、buttonBがそのまま追従してくれる。