WWDC2017のAuto Layout Techniques in Interface Builderのセッションのまとめになります。これはAutoLayoutを扱う上での6つのテクニックについて紹介するセッションです。この記事ではその概要と手元で検証した様子を紹介しようと思います。検証したコードはセッション中のコードと同一のものではないのでご了承ください。
※サンプルコードはもう少し付け足したり直していく予定です。
Changing layout at runtime
アプリ実行時に動的にレイアウトを変えたい時にどうするかという話です。制約を都合よく扱えるようにするために調整したい要素を上に乗せたviewを1枚作成してその制約及び関係する制約のisActiveフラグを切り替えることで思うようなレイアウトを実現するという手法が紹介されていました。一部を抜粋するとこんなかんじです。詳しいコードはサンプルコードを参照してください。
if shouldShow {
zeroHeightConstraint.isActive = false
edgeConstraint.isActive = true
} else {
edgeConstraint.isActive = false
zeroHeightConstraint.isActive = true
}
この方法自体はまあよくある話かなーと思ったので詳細は省略します。一つ、全然違うところで勉強になったのはweakのNSLayoutConstraintを一度isActive = falseにすると制約がnilになってしまうということです。この件に関しては以下のstackoverflowを参考にしました。
- Why weak IBOutlet NSLayoutConstraint turns to nil when I make it inactive?
- When can I activate/deactivate layout constraints?
Tracking touch
タッチを追跡する方法についてです。一般にviewの位置はframeによって定義され、制約が付いている場合はframeの値は制約から計算されます。しかしviewの位置に影響を与えるものとしてtransformプロパティがあります。これは制約からframeが計算された後に拡大、回転などを追加することができます。
デモでは左右にカードをポイするようなUIを例にして説明されています。第一段階としてpanジェスチャーをした時にviewが移動する(ただし手を離してもviewが元の位置に戻らない)、第二段階として手を離した時にviewが元の位置に戻るようにする、第三段階としてviewが元の位置に戻る時にバウンドするようなアニメーションを付けるということをしていました。
まとめとしては、viewの位置は複数のプロパティの組み合わせだということ、今回楽しいインタラクションを与えたのはたった数行のコードだということ、一時的な変更にはtransformが便利ということあたりでしょうか。こちらも詳しくはサンプルコードを参照してください。ピンク色のviewが動くようになっています。
Dynamic type
Dynamic typeは一連のテキストスタイルを提供するもので、ユーザは見出し、本文などのスタイルのサイズをコントロールすることができます。作り方によってはテキストは大きくなるがレイアウトが適切ではなく、読みにくいなどの状況が発生します。幸い、Interface Builderを使えば簡単に修正ができます。
UILabelを選択した状態で右ペインのDynamic Typeのチェックボックスにチェックを付けます。
このプロパティを有効にするには、固定のフォントサイズではなくテキストスタイルを指定する必要があります。Fontプロパティの「T」ボタンをクリックしてSystemのものではなくText Stylesのどれかを選択します。
※補足:コードで指定する場合の最低限の実装例は以下の通りです。
if #available(iOS 10.0, *) {
dynamicTypeLabel.adjustsFontForContentSizeCategory = true
dynamicTypeLabel.font = UIFont.preferredFont(forTextStyle: .headline)
} else {
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil, queue: nil, using: { _ in
self.dynamicTypeLabel.font = UIFont.preferredFont(forTextStyle: .headline)
})
}
実機(iOS 10.2.1)で確認したらこんなかんじになりました。
※補足:iOS 9以前ではフォントサイズの変更を監視するコードを追加しなければならないようです。
デフォルトサイズ | 最大サイズ |
---|---|
シミュレータの場合、設定アプリを起動するのは面倒なので別の方法で文字サイズを変更します。Depeloper ToolからAccessibillity Inspectorを起動します。
するとこんな画面が現れるのでSimulatorをターゲットにします(実機を繋いでいる時はシミュレータはターゲットに出てきませんでした。ターゲットを実機にすれば実機のフォントサイズもここで変えられます)。
右上の設定ボタンをクリックするとスライダーが出てきて、シミュレータを横に並べつつ表示を確認するということができます。
最小サイズ | 最大サイズ |
---|---|
このように便利にフォントサイズを変更することができますが、大きくしすぎるとラベルが重なり合ってしまうので今度はそれを修正します。
デフォルトサイズ | 最大サイズ |
---|---|
今2つのラベルが乗っているviewには高さの制約が付いています。
これを、高さの制約の代わりにtop, bottom, 2つのラベルの間の制約を使うことで調整します。
2つのラベル間の制約は新しくできたVertical Baseline Standard Spacingを用いることができます。これはラベルのText Styleとフォントサイズに応じて制約の定数を調節すること以外はVertical Spacingと変わりません。
この制約を加えてから実行すると先程より良くなっています。
調整前 | 調整後 |
---|---|
より詳しい情報はBuilding Apps with Dynamic Typeで得られます。
さらに大きな文字を使用する場合はアクセシビリティを利用します。
Safe areas
iOS 11からtop及びbottom layout guideがdeprecatedになって新しくUIViewのプロパティとしてSafe Area Layout Guideというものができます。これによりナビバー、タブバーの中央に配置するなどの操作がしやすくなります。高さを変えたりlandscapeを変えたりしてもそれに応じて配置してくれます。Safe Area Layout GuideはtvOSでも適用することができます。考慮しなければならないことはディスプレイ幅のバリエーションが増えるということと、オーバースキャンに関してです。
Safe Area Layout Guideを有効にする方法は簡単で、file inspectorでUse Safe Area Layout Guidesのチェックボックスにチェックを入れるだけです。しかも後方互換性があります。
ここから先はこちらの記事を参考にしつつ自分でも検証してみた内容になります。
コードでSafe Area Layout Guideを設定する場合は以下のようになります。
private let sampleView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
sampleView.translatesAutoresizingMaskIntoConstraints = false
sampleView.backgroundColor = .brown
view.addSubview(sampleView)
let margins = view.layoutMarginsGuide
NSLayoutConstraint.activate([
sampleView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
sampleView.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
])
if #available(iOS 11, *) {
let guide = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
sampleView.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0),
guide.bottomAnchor.constraintEqualToSystemSpacingBelow(sampleView.bottomAnchor, multiplier: 1.0)
])
} else {
let standardSpacing: CGFloat = 8.0
NSLayoutConstraint.activate([
sampleView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: standardSpacing),
bottomLayoutGuide.topAnchor.constraint(equalTo: sampleView.bottomAnchor, constant: standardSpacing)
])
}
}
また、記事によるとiOS 11において新しくconstraintEqualToSystemSpacingBelow
、constraintEqualToSystemSpacingAfter
が追加されたおかげでstandard spaceをハードコードする必要がなくなったと書かれています。
ちなみにUIViewControllerの方にもsafeAreaLayoutGuideプロパティがあったのですがすでにdeprecatedになっています。上記で使っているのはUIViewのsafeAreaLayoutGuideです。
Proportional proposition
superviewに対して相対的に位置付けする際のテクニックについての話になります。あるviewを高さ70%の大きさに縮めたい時、spacer viewを用いて調節する方法を紹介しています。
高さ70%のspacer viewを作るためにはいったんsuperviewに対してEqual Heightの制約を張ります。
今回はsuperviewに対して70%ではなくSafe Areaに対して70%の制約を張りたいので、右ペインの制約設定でSuperview.HeightをSafe Area.Heightに変更します。
最終的な制約の状況は次の通りです。
結果的に端末を変えても、縦横を変えても70%という比率を守ったレイアウトができます。
もしInterface Builderではなくコードで同様のことをしたい場合はUILayout Guideを使います。
Stack view adaptive layout
stack viewを用いて理想に合ったレイアウトを作成していく方法についてです。今回の例では縦と横でレイアウトを変える実装をします。
細かいところは省いてかいつまんで説明します。
まずは以下のように準備をします。stack viewの中身は等間隔にしたいのでDistributionをFill Equallyにしています。
ここから自分が望むレイアウトになるようにカスタマイズします。Xcode9, iOS 11からは標準の間隔オプションがあるのでそれを選択してみます。
次にview4つを正方形にします。あるviewから自身に対しAspect Ratioの制約をかけます。
ただしこのままだと制約エラーがおきます。試しにinstalledプロパティのチェックを外すとエラーはなくなりますがsuper viewにフィットしなくなります。
そこでtop、trailingの制約をON/OFFしてみます。OFFにすることで制約エラーはなくなり4つのviewは正方形になりますが、やはりsuper viewにフィットしない状態(余白がおかしいなど)になってしまいます。
ここで、viewを正方形にすることとFill Equallyを同時に満たすことができないのだということがわかります。今回はsuper viewの全体を活用する必要はないのでbottomの制約を外すことによってエラーを解決することはできますが、landscapeのときのことを考えGreater Than or Equalにしておきます。
ただしこれだけだとlandscapeにした時にエラーが発生するので修正していきます。
Xcode9で新しく登場したサイズクラスのHiddenプロパティを活用します。stack viewでhiddenプロパティを使うとレイアウトは崩れるものの配置はキープできます。
HeightがCompactの時にはUILabelをHiddenにする設定をします。
するとlandscapeの時はUILabelが見えなくなります。あとは制約を修正したり、上記と同じ要領でlandscapeの時だけ右側にtext viewを表示するようにしていきます。
まとめとしては、使えるときはalignment, distribution, spacingを駆使してstack viewを使って使うと良い、stack viewを使うと少ない制約で済ませることができる、Xcode9におけるhiddenプロパティはでsize classに対応しているということです。