読者です 読者をやめる 読者になる 読者になる

AutoLayoutで状況に応じた制約を適用する術

iOS Swift

やりたいこと

  • 2つのViewを条件によって1つ表示したり2つ表示したり
  • 1つの場合は、親Viewのサイズいっぱいで
  • 2つの場合は、良い感じにマージンを効かせて横並びに同じサイズで親Viewいっぱいに

サンプルコード

レイアウトはコードで書こうマンなのでサンプルコードもコードにて。 SnapKitにどっぷり。 Interface Builderを使う場合は、矛盾する制約をつけつつデフォルト状態を表現する制約意外を無効にしておきつつ、各制約をアウトレットに繋ぐ感じになるはず。

import UIKit
import SnapKit

class ViewController: UIViewController {
    let hogeView = HogeView()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.hogeView.show1()  // どちらか
        self.hogeView.show2()  // 片方を適用
    }

    override func loadView() {
        super.loadView()
        self.view.backgroundColor = UIColor.whiteColor()

        self.view.addSubview(hogeView)
        self.hogeView.snp_makeConstraints { (make) -> Void in
            make.left.top.equalTo(self.view).offset(32)
            make.right.bottom.equalTo(self.view).inset(32)
        }
    }
}

class HogeView: UIView {
    let view1 = UIView()
    let view2 = UIView()

    private var constraints1: [Constraint] = []
    private var constraints2: [Constraint] = []

    init() {
        super.init(frame: CGRectZero)

        self.addSubview(self.view1)
        self.addSubview(self.view2)
        self.view1.backgroundColor = UIColor.greenColor()
        self.view2.backgroundColor = UIColor.redColor()

        // 1個の場合
        self.view1.snp_makeConstraints { (make) -> Void in
            self.constraints1.append(make.left.right.top.bottom.equalTo(self).constraint)
        }
        self.view2.snp_makeConstraints { (make) -> Void in
            self.constraints1.append(make.right.bottom.equalTo(self).constraint)  // サイズが0だから適当なところに置く
            self.constraints1.append(make.width.height.equalTo(0).constraint)
        }

        // 2個の場合
        self.view1.snp_makeConstraints { (make) -> Void in
            self.constraints2.append(make.left.top.bottom.equalTo(self).constraint)
            self.constraints2.append(make.height.equalTo(self).constraint)
        }
        self.view2.snp_makeConstraints { (make) -> Void in
            self.constraints2.append(make.right.top.bottom.equalTo(self).constraint)
            self.constraints2.append(make.left.equalTo(self.view1.snp_right).offset(4).constraint)
            self.constraints2.append(make.width.height.equalTo(self.view1).constraint)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func show1() {
        for constraint in self.constraints1 {
            constraint.activate()
        }
        for constraint in self.constraints2 {
            constraint.deactivate()
        }
    }

    func show2() {
        for constraint in self.constraints1 {
            constraint.deactivate()
        }
        for constraint in self.constraints2 {
            constraint.activate()
        }
    }
}

サンプル動作

1個の場合

f:id:mihyaeru21:20151028003921p:plain

2個の場合

f:id:mihyaeru21:20151028003932p:plain

解説的やつ

2パターンの制約を作っておいて、条件によって有効にする方を切り替える感じのアプローチ。 それだけ。

2個の場合の制約の説明をすると、view1の横幅はview2で設定するから設定しない。 view2は縦横幅がview1と同じで、さらにview1とview2の間には4のスペースが入るので、良い感じに横幅を2分割しつつ間にスペースが入るという寸法。

まとめ

いちいち制約を配列にぶち込んで、それを後でループして頑張る、というスマートではない方法だけど要件は満たす事ができた。 もっとスマートな方法があったら知りたい。

追記

矛盾する制約を設定する前に全部deactivateしておかないと警告が出まくっていた。 こんなextensionを作って self.constraints1.deactivateAll() とかして事なきを得た。

extension Array where Element: Constraint {
    func activateAll() {
        for constraint in self {
            constraint.activate()
        }
    }

    func deactivateAll() {
        for constraint in self {
            constraint.deactivate()
        }
    }
}

今回の例みたいな規模なら、全ての制約を入れ替えることなく一部の制約だけで対処できるはず。 条件によって配置が大幅に変わる場合は今回の方式でやるのが良いかなと。