🥦

Auto Layout の基礎

2021/10/27に公開

はじめに

iOS は最近始めたばかりで、まず AutoLayout を理解するところで苦労した。基礎的なところをまとめてみましたので、初学者の方の参考にでもなればと思い紹介します。

macOS Mojava 10.14.6 / Xcode 11.3.1 / Swift 5.0

基本的な考え方

ビューにコントロールを配置するとき、コントロールの位置と大きさは、一般的にはコントロールの frameプロパティ(ビュー上の座標点とコントロールを格納する矩形領域のサイズからなる)により定義する。iPhoneは機種によってスクリーンの大きさが異なるので、この方法をとるとコントロールがスクリーンからはみ出したり、余白が偏って生じたりする場合がある。

Auto Layoutoは従来型のコントロールの配置とは全く違う方法である。 この方法をとることにより、スクリーンの大きさが異なっても、コントロールが自動的に適正な位置に配置されるよう指定することができる。

一言で言えば、Auto Layout を使いこなすには、制約(constraint)の意味を理解すれば良い。下記の図に示すが、例えば、スパービューの中にひとつのビューを配置するとする。まず、水平方向の構成として、X軸は、(1) スーパービューの左端からビューの左端までの線分、(2) ビューの幅の線分、(3)ビューの右端からスーパービューの右端までの線分に3分割することができる。

このとき、これらの線分のうち二つの長さを固定すれば、スーパービューの幅が変化したとしても、その変化は残りのひとつの線分の増減によって吸収することができる。

固定値がひとつでは、スーパービューの幅の変化を残りのどちらの線分で吸収したらよいかわからない。また三つとも固定値であれば、幅の変化に対応できない。いずれも Auto Layout の定義としてはエラーである。

これは垂直向においても同様である。Y軸の線分は、 (1) スーパービューの上端からビューの上端までの線分、(2) ビューの高さの線分、(3)ビューの下端からスーパービューの下端までの線分に3分割できる。

以上で説明した特定の線分に固定値を割り当てることを「制約を設定する」という。水平方向、垂直方向の制約を組み合わせて、ビューを配置する例を次に示す。

制約の設定方法

ビューの四辺を表すキーワードは、左端は leading、右端は trailing、上端は top、
下端は bottom となる。

以下コードで制約を設定する例を示す。
ビューの左端とスーパービューの左端の間隔を10ピクセルに固定する。

view.leading = superView.leading + 10

ビューの右端とスーパービューの右端の間隔を10ピクセルに固定する。方向があるのでマイナス値になる。

view.trailing = superView.trailing - 10

ビューの上端とスーパービューの上端の間隔を10ピクセルに固定する。

view.top = superView.top + 10

ビューの下端とスーパービューの下端の間隔を10ピクセルに固定する。方向があるのでマイナス値になる。

view.bottom = superView.bottom - 10

ビューの幅を100ピクセルに固定する。

view.width = 100

ビューの高さを100ピクセルに固定する。

view.height = 100

ビューの位置を中央に固定する

view.centerX = superView.centerX
view.centerY = superView.centerY

制約の設定例

制約を適切に設定することによって、以下に示す例のようなレイアウト設定を行うことができる。

例1

水平方向はビューの両側に制約を設定し、ビューの幅を可変とする。垂直方向はビューの上下側に制約を設定し、ビューの高さを可変とする。スーパービューの大きさの変化に応じて中のビューのサイズが変化する。

例2

水平方向はビューの左側と幅に制約を設定し、ビューの右側を可変とする。垂直方向はビューの上側と高さに制約を設定し、ビューの下側を可変とする。スーパービューの大きさの変化に応じてビューの右側と下側の余白の大きさが変化する。

例3

水平方向と垂直方向の中心を固定し、ビューの幅と高さを指定している。スーパービューの大きさが変わってもビューは常に中心に表示される。

例4

ビュー間の制約は、スーパービューとサブビューの間だけでなく、同じ階層のビュー間でも設定することができる。それを利用して、スーパービューの中に複数のビューを配置し連動させてみる。

次の例は、二つのビューが水平方向に並列していて、水平方向の制約はビューAの左側、ビューBの両側、およびビューBの幅に設定する。ビューAからビューBへと同階層のビュー間に制約を設定している。スーパービューの幅が変化すると唯一可変のビューAの幅が変化する。また、ビューBの上側は、ビューAの上側と同じ高さになるよう制約を設定している。スーパービューの高さが変化すると、ビューAの高さとビューBの下の余白が変化する。

viewA.leading + superView.leading + L1
viewB.leading = viewA.trailing + L2
viewB.width = L3
viewB.trailing = superView.trailing - L4
viewA.top = viewB.top

サンプル アプリケーション

Auto Layoutによりビューとラベルを配置し、機種によって表示がどのように変わるか確認する。Auto Layoutの定義は Interface Builderを利用するのが一般的かもしれないが、ここでは全てコードにより実装してみた。個人的にはこの方が理解しやすい。警告等のサジェスチョンが得られないのはデメリットだが。

ViewController
import UIKit

class ViewController: UIViewController {
    let view1 = UIView()
    let view2 = UIView()
    let view3 = UIView()
    
    let imgView = UIImageView()

    let label1 = UILabel()
    let label2 = UILabel()
    let label3 = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        //ビューの属性設定
        view1.backgroundColor = UIColor.red
        view2.backgroundColor = UIColor.lightGray
        view3.backgroundColor = UIColor.blue
        self.view.addSubview(view1)
        self.view.addSubview(view2)
        self.view.addSubview(view3)
        //イメージビューの属性設定
        imgView.backgroundColor = UIColor.black
        if let image:UIImage = UIImage(named:"sakura.png"){
            imgView.image = image
        }
        self.view.addSubview(imgView)
        //ラベルのの属性設定
        label1.text = "左側"
        label2.text = "中央"
        label3.text = "右側"
        label1.font = UIFont.systemFont(ofSize: 30)
        label2.font = UIFont.systemFont(ofSize: 30)
        label3.font = UIFont.systemFont(ofSize: 30)
        label1.textAlignment = .center
        label2.textAlignment = .center
        label3.textAlignment = .center
        label1.textColor = UIColor.white
        label3.textColor = UIColor.white
        label1.backgroundColor = UIColor.red
        label2.backgroundColor = UIColor.lightGray
        label3.backgroundColor = UIColor.blue
        self.view.addSubview(label1)
        self.view.addSubview(label2)
        self.view.addSubview(label3)

        //制約の設定の準備
        imgView.translatesAutoresizingMaskIntoConstraints = false
        view1.translatesAutoresizingMaskIntoConstraints = false
        view2.translatesAutoresizingMaskIntoConstraints = false
        view3.translatesAutoresizingMaskIntoConstraints = false
        label1.translatesAutoresizingMaskIntoConstraints = false
        label2.translatesAutoresizingMaskIntoConstraints = false
        label3.translatesAutoresizingMaskIntoConstraints = false
        //制約の設定
        let constraints = [
            //左側のビュー
            view1.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
            view1.widthAnchor.constraint(equalToConstant: 70),
            view1.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 100),
            view1.heightAnchor.constraint(equalToConstant: 100),
            //中央のビュー
            view2.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
            view2.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 100),
            view2.bottomAnchor.constraint(equalTo: imgView.topAnchor, constant: -75),
            view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
            //右側のビュー
            view3.widthAnchor.constraint(equalToConstant: 70),
            view3.heightAnchor.constraint(equalToConstant: 50),
            view3.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 100),
            view3.leadingAnchor.constraint(equalTo: self.view2.trailingAnchor),
            view3.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
            //イメージビュー
            imgView.widthAnchor.constraint(equalToConstant: 160),
            imgView.heightAnchor.constraint(equalToConstant: 120),
            imgView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
            imgView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor),
            //左側のラベル
            label1.widthAnchor.constraint(greaterThanOrEqualToConstant: 70),
            label1.heightAnchor.constraint(equalToConstant: 50),
            label1.topAnchor.constraint(equalTo: imgView.bottomAnchor, constant: 75),
            label1.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
            //中央のラベル
            label2.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
            label2.heightAnchor.constraint(equalToConstant: 50),
            label2.topAnchor.constraint(equalTo: label1.topAnchor),
            label2.leadingAnchor.constraint(equalTo: label1.trailingAnchor),
            //右側のラベル
            label3.widthAnchor.constraint(greaterThanOrEqualToConstant: 70),
            label3.heightAnchor.constraint(equalToConstant: 50),
            label3.topAnchor.constraint(equalTo: label1.topAnchor),
            label3.leadingAnchor.constraint(equalTo: label2.trailingAnchor),
            label3.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor)
        ]
        NSLayoutConstraint.activate(constraints)
        //優先度
        label1.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
        label2.setContentHuggingPriority(UILayoutPriority.defaultLow, for: NSLayoutConstraint.Axis.horizontal)
        label3.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: NSLayoutConstraint.Axis.horizontal)
    }
}

[補足]

UILabelの幅の範囲指定

Auto Layoutでラベル(テキストフィールド)はやや特殊な動きをする。ラベルの幅に固定値を設定すると「Fixed width constraints may cause clipping.」という警告(注1)がでる。これは、ラベルに表示する文字列の長さは実行前には不定なので、場合によっては文字列が切れてしまいますよという意味。

(注1)Inrefface Builderのレイアウト設定のときにでるだけで、コードで指定した場合には出ない。

この警告を消すためには、ラベルの幅を可変(greater than or equal to)にし、かつ、ラベルの左右いずれかの側の間隔を可変(less than or equal to)にする。これで文字列に長さに合わせてラベルの幅を可能な限り広げて表示することができる。

下記の図の例ではラベルの幅は最小100ピクセルだが、文字列の長さに応じてスーパービューの右端まで拡張される。

ただしこの警告は、ラベルに表示する文字列の長さがが事前にアプリケーションで分かっていて、設計において充分な幅をとっていれば無視しても構わないと考える。

範囲指定の優先度

上記アプリケーションの下段に表示している「左側」「中央」「右側」はラベルである。両側のラベルは 70ピクセル固定とし、中央のラベルはスクリーンの幅に合わせその残余の幅にしている。

上記の警告をださないようにするには、制約の設定は
左のラベルが Widtth >= 70, 中央のラベルが Widtth >= 0, 右のラベルが Widtth >= 70 となる。

ただしこうすると同じ条件の設定が複数あり、デフォルトでは左側からラベルの幅が確定していくので次のような表示になってしまう。

これに対応するには、制約の実行の優先度を ContentHuggingPriority により個別に指定することである。本例では、両側のラベルの優先度を中央のラベルのそれより高くしている。こうすれば、まず両側のラベルが 70ピクセルで確定し、そのあと中央のラベルの幅が決まる。詳細はコードを参照のこと。

Discussion