[iOS] AutoLayoutの制約付け替えでレイアウトを動的に変える!active/deactiveのお話

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんぬづは、シン・ゴジラの実況ツイート(#シンゴジ実況)にハマっている田中です。
キャスト陣がのっかったツイートをしているのを見るとテンションあがる。

今回はAutoLayoutの制約を動的に付け替えたいというお話。

AutoLayoutの制約を動的に付け替えるとは

この記事で言う制約を動的に付け替えるというのは以下のようなレイアウトのお話。
Twitterクライアントアプリのセルのようなものをイメージしてください。
画像が1枚のみだった場合は横幅いっぱいとし、画像が2枚以上だった場合は横幅半分とします。

スクリーンショット_2016-11-11_11_51_25 スクリーンショット_2016-11-11_11_51_58

該当するUIImageViewの親Viewに対するEqual Widthの制約をmultiplierで制御している例です。
NSLayoutConstraintのmultiplierプロパティは親ビューのattributeにかかる倍率を表すパラメータですが、このmultiplierプロパティはget onlyで宣言されています。

open var multiplier: CGFloat { get }

そのため一つのNSLayoutConstraintインスタンスのmultiplierの値を変更することで動的にmultiplier1.0とmultiplier0.5(半分の横幅)を切り替えることはできないという問題があります。
multiplierの値を書き換えることはできないので、都度付け替えるようにしようとするとどうするのかという方法を紹介していきます。

対応策

二つのEqual Widthを設定する

一つのNSLayoutConstraintのmultiplierを書き換えることはできないので、二つのEqual Widthを重ねがけして片方のmultiplierを1.0、もう片方のmultiplierを0.5で設定します。

Main_storyboard_—_Edited

ただし同じ制約をかけるとコンフリクトしてしまいます。
次にこのコンフリクトを解消します。

デフォルトとなる制約を設定する

コンフリクトを解消するためにデフォルトとなる制約を設定する必要があります。
デフォルトとなる制約を設定するにはInterface Builder上で制約を選択しInstalledの項目にチェックを入れます。(制約を追加した段階ではどの制約にもInstalledにチェックが入っている状態です)
今回はmultiplier1.0の制約をデフォルトとします。
そしてデフォルト設定とならないmultiplier0.5の制約に対してはInstalledの項目のチェックを外します。

Main_storyboard_—_Edited1 Main_storyboard_—_Edited2

これでコンフリクトは解消されます。

制約のactive/deactiveを切り替えて制約を場合分けする

現状では常にデフォルト設定としたmultiplier1.0のフルサイズのUIImageViewとなります。
そこで制約のactivateとdeactivateを切り替えることをコードで書いていきます。
切り替えにはNSLayoutConstraintのクラスメソッドである

  • open class func deactivate(_ constraints: [NSLayoutConstraint])
  • open class func activate(_ constraints: [NSLayoutConstraint])

の二つのメソッドを使用します。

下記のコードは動的に制約のactive/deactiveを切り替えるコードです。
imageViewWidthConstraintimageViewWidthHalfConstraintは先ほど設定した二つの制約をIBOutletで繋いだものになります。

class TableViewCell: UITableViewCell {
	
	@IBOutlet weak var imageViewWidthConstraint: NSLayoutConstraint!
	@IBOutlet weak var imageViewWidthHalfConstraint: NSLayoutContraint!

	func fill(with data: DataModel) {
		if data.imageURLs.count == 1 { // 画像の枚数が1枚の時

			NSLayoutConstraint.deactivate([imageViewHalfWidthConstraint])
			NSLayoutConstraint.activate([imageViewWidthConstraint])

		} else data.imageURLs.count > 1 { // 画像の枚数が2枚以上の時

			NSLayoutConstraint.deactivate([imageViewWidthConstraint])
			NSLayoutConstraint.activate([imageViewHalfWidthConstraint])

		}
	}

}

TableViewCellクラスにDataModelを渡してUITableViewCellの設定を行うfunc fill(with data: DataModel)内でその変更を記述します。
画像の枚数が1枚だった場合はUIImageViewの横幅をいっぱいにするためにmultiplier1.0の制約を有効(activate)にし、横幅半分のmultiplier0.5の制約を無効(deactivate)にします。
画像の枚数が2枚以上だった場合はUIImageViewの横幅をいっぱいにするためにmultiplier1.0の制約を無効(deactivate)にし、横幅半分のmultiplier0.5の制約を有効(activate)にします。

これで制約を動的に付け替えることができました!

スクリーンショット 2016-11-11 14.44.07

まとめ

もし今回のように二つの制約を付け替えるのみでく、IBOutletによって紐付けた制約でなくコードでつけた制約であったりしても、その制約をViewから探索する関数を書いて取得し、activate/deactivateを切り替えてあげればより動的な制約の変更が可能になります。
削除して追加するなどでなく、activate/deactivateの切り替えをすれば済むので計算コストを抑えた作りができそうですね。

余談

Interface Builder上で色の薄くなった制約について

制約を追加したり付け替えたりしていると、下の画像のように色の薄くなる制約が時折現れます。

スクリーンショット 2016-11-11 14.57.54

今回のactive/deactiveの話を学んだ上で制約が有効かどうかを表している意味であるということがわかりました。
先述したInstalledのチェックをつけたりはずしたりすることで確認できます。

知らなかった。

参考