こんにちはGunosy 新規事業開発室の広瀬 (@aikizoku) です。
最近は細々とSwiftやGoやPython書いています。

この記事はGunosy Advent Calendar 2017の2日目の記事です。
昨日の記事は@mathetakeさんのGunosyのパーソナライズを支える技術 -計算モデルとアーキテクチャ編-でした。

さて、今更かもですが本日は多くのiOS開発者が触っているAutoLayoutについて書きたいと思います。
AutoLayoutは慣れると非常に便利で有用ですが、慣れるまではとっつきにくいツンデレな機能だと思っております。

そこで出来るだけ多く方々にAutoLayoutに慣れて幸せになって頂くように、自分が現場で役立ったTips集を残していこうと思います。

(慣れるまでは)この方法でレイアウトすると楽ですよ

1.縦横に伸びた時のイメージを絵に書く

Re8Rqn0jvacRJCS1512025445_1512025517.jpg
* 左Viewサイズは1:1を保って縦横サイズ可変
* 上Viewは縦横サイズ可変
* 下Viewは縦サイズ固定、横サイズ可変

2.絵の通りにViewを仮設置してみる

まずは何の制約もかけずに、愚直にそれっぽく配置してみましょう
スクリーンショット 2017-11-30 16.17.23.png

3.制約を設定していく

続いて落ち着いて1つづつ制約を設定していきましょう。
仮設置がしっかりできていれば、制約をかける対象のViewがデフォルトでいい感じに設定されているので楽にできます。
スクリーンショット 2017-11-30 16.22.35.png
* Constraint to marginsにチェックが入っていたら、特に意図しないのであれば余計なmarginが入ってしまうので外しておきましょう。

4.失敗したりわからなくなったらすぐにリセットする

AutoLayoutのUIは慣れるまではとっつきにくく、ミスがあってもどこかわからない事が多いです。
なので無理しないで失敗したりわからなくなったらリセットして最初からやりましょう。
急がば回れです。
スクリーンショット 2017-11-30 16.28.22.png

コードで書きたい!

AutoLayoutはコードでも書けます。
実務だとどうしてもコードで書かなければならない箇所(動的に値を変えたり等)が出てくるので
覚えておくと良いと思います。

// サンプルのViewを作成します
let subView = UIView(frame: CGRect.zero) // AutoLayoutを使うと制約でframeが決まるので値は何でも良いです。
subView.backgroundColor = UIColor.blue // わかりやすいように青色にします

// サンプルのViewを準備します
subView.translatesAutoresizingMaskIntoConstraints = false // Autoresizingを無効にしないとAutoLayoutが有効になりません。
self.view.addSubview(subView) // AutoLayoutを設定する前に、必ずaddSubViewする必要があります。

やりたい事

左右に10pxづつ隙間を空けて高さを200pxにして中央に設置する
 ①左に10px隙間を作る
 ②右に10px隙間を作る
 ②高さを200pxにする
 ③縦方向に中央揃えする
上記を以下の4つの方法でご紹介していきます。

方法1:NSLayoutConstraintを使って書く その1

// ①
self.view.addConstraint(
    NSLayoutConstraint(
        item: subView,
        attribute: .leadingMargin,
        relatedBy: .equal,
        toItem: self.view,
        attribute: .leadingMargin,
        multiplier: 1.0,
        constant: 10)
)

// ②
self.view.addConstraint(
    NSLayoutConstraint(
        item: subView,
        attribute: .trailingMargin,
        relatedBy: .equal,
        toItem: self.view,
        attribute: .trailingMargin,
        multiplier: 1.0,
        constant: -10)
)

// ③
self.view.addConstraint(
    NSLayoutConstraint(
        item: subView,
        attribute: .height,
        relatedBy: .equal,
        toItem: nil,
        attribute: .height,
        multiplier: 1.0,
        constant: 200)
)

// ④
self.view.addConstraint(
    NSLayoutConstraint(
        item: subView,
        attribute: .centerY,
        relatedBy: .equal,
        toItem: self.view,
        attribute: .centerY,
        multiplier: 1.0,
        constant: 0)
)

はい、とても長いですね。この時点でAutoLayoutが嫌いになりますよね(えっ
一番レガシーな書き方でライブラリ化して簡易化させたい感がハンパないコードですが
AutoLayoutが登場したiOS5から使えるという利点があります。

方法2:NSLayoutConstraintを使って書く その2

// ①, ②
self.view.addConstraints(
    NSLayoutConstraint.constraints(
        withVisualFormat: "H:|-leadingMargin-[subView]-trailingMargin-|",
        options: [],
        metrics: ["leadingMargin": 10, "trailingMargin": 10],
        views: ["subView": subView])
)

// ③
self.view.addConstraints(
    NSLayoutConstraint.constraints(
        withVisualFormat: "V:[subView(height)]",
        options: [],
        metrics: ["height": 200],
        views: ["subView": subView])
)

// ④
// ※色々試しましたがoptionsのalignAllCenterYが効かず、方法1と併用しました
self.view.addConstraint(
    NSLayoutConstraint(
        item: subView,
        attribute: .centerY,
        relatedBy: .equal,
        toItem: self.view,
        attribute: .centerY,
        multiplier: 1.0,
        constant: 0)
)

少しだけ短く書けましたね。
その代わりにちょっと見慣れない記法でレイアウトしています。
こちらはVisualFormatLanguageと呼ばれるレイアウトをアスキーアートチックに書ける記法を用いています。
少し慣れれば直感的にレイアウトがわかるようになるので便利ですが、文字列として読み込まれるのでエラーが実行してクラッシュするまでわからないという欠点があります。
エラー内容も「文法が間違っているぞ」で以上終了なので複雑なレイアウトだとバグ探しも一苦労です。

方法3:NSLayoutAnchorを使って書く

// ①
subView.leadingAnchor.constraint(
    equalTo: self.view.leadingAnchor,
    constant: 10
    ).isActive = true

// ②
subView.trailingAnchor.constraint(
    equalTo: self.view.trailingAnchor,
    constant: -10
    ).isActive = true

// ③
subView.heightAnchor.constraint(
    equalToConstant: 200
    ).isActive = true

// ④
subView.centerYAnchor.constraint(
    equalTo: self.view.centerYAnchor
    ).isActive = true

方法1と比べると非常にシンプルで書けますね。
こちらはiOS9で追加されたNSLayoutAnchorを使った書き方です。
短いコードで書けて読みやすいので個人的におすすめです。
iOS9未満に対応する時は使えないのでご注意ください。

方法4:Cartographyを使って書く

import Cartography

constrain(self.view, subView) { (view, subView) in
    // ①
    subView.leadingMargin == view.leadingMargin + 10

    // ②
    subView.trailingMargin == view.trailingMargin - 10

    // ③
    subView.height == 200

    // ④
    subView.centerY == view.centerY
}

AutoLayoutを簡単に書けるライブラリは多々ありますが、有名所でいうとこのCartoraphyでしょうか。
多くの方に支持されたライブラリにふさわしい、シンプルに書けてでコードも見やすくロジックも入れやすい素晴らしいインターフェースですね。

まとめ

個人的には少しレイアウトをコードで書く程度でしたら方法3を使い、がっつりとコードで書く機会が多そうでしたら方法4を選ぶようにしています。

コードとInterfaceBuilderを両方使って書きたい!

よくあるパターンが基本的なレウアウトはInterfaceBuilderで行い、動的な変更をコード上で行うという方法です。
そういう時はLabelやButtonと同様に、制約をIBOutletとして定義すると便利です。

// 例:InterfaceBuilderで定義した幅の制約を取得する
@IBOutlet weak var widthConstraint: NSLayoutConstraint!

// 幅の長さを変更する
widthConstraint.constant = 10

アニメーションさせたい!

AutoLayoutでアニメーションする時は少し特殊な書き方をします。

// 幅の長さを変更する
widthConstraint.constant = 10

// アニメーションブロックスの中でlayoutIfNeededを呼ぶ
UIView.animate(withDuration: 1.0) {
    subView.layoutIfNeeded()
}

トルツメしたい!

アプリを開発していると「テキストが空の時はこのラベルをトルツメしたい!」というような事がよくあると思います。
しかし、AutoLayoutされたものはただhiddenすれば消えるというわけではありません。
ちょっと工夫が必要です。

  • 方法1:UIStackViewを使う
    Androidエンジニアの方には「LinearLayoutだよ」というとわかりやすいかと思います。
    UIStackViewの中にレイアウトをすれば、内部のViewにisHidden=trueするとそのViewがトルツメされます

  • 方法2:isHidden=trueにしたら縦0または横0の制約をかける
    応急処置感ハンパないですが、ViewをisHidden=trueした後に縦0または横0の制約をかけて無理矢理消すという方法もあります。

黒魔術を使っているのであまりよろしくないですが、下記Exensionを使うと楽です

Extension

extension UIView: HasAssociatedObjects {

    /**
     AndroidのView.GONE(hiddenしてトルツメ)を再現する
     */
    private var goneConstraint: NSLayoutConstraint? {
        get {
            return associatedObjects["goneConstraint"] as? NSLayoutConstraint
        }
        set {
            associatedObjects["goneConstraint"] = newValue
        }
    }

    var gone: Bool {
        get {
            return goneConstraint == nil
        }
        set {
            if newValue && goneConstraint == nil {
                let goneConstraint =
                    NSLayoutConstraint(item: self,
                                       attribute: .height,
                                       relatedBy: .equal,
                                       toItem: nil,
                                       attribute: .notAnAttribute,
                                       multiplier: 1,
                                       constant: 0)

                addConstraint(goneConstraint)
                self.goneConstraint = goneConstraint
                isHidden = true
            } else if goneConstraint != nil {
                removeConstraint(goneConstraint!)
                goneConstraint = nil
                isHidden = false
            }
        }
    }
}

/**
 extensionでStored Propertyを使うための黒魔術
 */
protocol HasAssociatedObjects {
    var associatedObjects: AssociatedObjects { get }
}

private var AssociatedObjectsKey: UInt8 = 0

extension HasAssociatedObjects where Self: AnyObject {
    var associatedObjects: AssociatedObjects {
        guard let associatedObjects = objc_getAssociatedObject(self, &AssociatedObjectsKey) as? AssociatedObjects else {
            let associatedObjects = AssociatedObjects()
            objc_setAssociatedObject(self, &AssociatedObjectsKey, associatedObjects, .OBJC_ASSOCIATION_RETAIN)
            return associatedObjects
        }
        return associatedObjects
    }
}

class AssociatedObjects: NSObject {
    var dictionary: [String: Any] = [:]
    subscript(key: String) -> Any? {
        get {
            return dictionary[key]
        }
        set {
            dictionary[key] = newValue
        }
    }
}

使い方

subView.gone = true

クラッシュしたよ〜 or 動かないよ〜 ウワァァンヽ(`Д´)ノ

次を疑いましょう
・クラッシュ
 ・制約をかけるViewをaddSubViewする前に制約を設定していないか?
 ・VisualFormatLanguageを使っている場合、文法は間違っていないか?
 ・制約を設定するViewを間違えていないか?特にsuperViewとsubViewを逆にしていないか
・動かない
 ・制約をかけるViewのtranslatesAutoresizingMaskIntoConstraintsをfalseにし忘れていないか
 ・制約が不足していたり矛盾していたりしないか?
  どうしてもわからない時は一度InterfaceBuilderで同様の制約を書けてみてエラーが出ないか確認してみるとよい

他にも色々あるのですが、あまりブログが長くなってしまってもあれなのでまた次回にします。