2015年11月19日木曜日

NSLayoutConstraintをコードで変更するお話

バリバリとStoryboardで制約をつけて開発...している毎日ですが、動的に制約内容を変えたくなる場面が多々発生します。
ボタンを押したら、このViewのサイズをググッと大きくする とか。

そうしたい時は、制約(NSLayoutConstraint)をViewControllerに紐付けて、ViewControllerで書き換えます。
view.frame で直接サイズを設定して変えることもできますが、元に戻ってしまうことがあるので、使わないほうがいいと思います。

書き換える方法は、NSLayoutConstraintのconstant(値)を変えればOK。

@IBOutlet weak var myConstraint: NSLayoutConstraint!

func changeSize() {
    myConstraint.constant = 20
    self.view.layoutIfNeeded()
}
でいける。
アニメーション付きでやりたい場合はlayoutIfNeededをアニメーション実行してあげればよし。
UIView.animateWithDuration(0.3, animations: { () -> Void in
    self.view.layoutIfNeeded()
 })
...が!
myConstraint.constant = 20
のように、値をハードコーディングしたくないですよね。
Storyboard上でせっかくLayoutを完結させているのに、コードでLayoutの固定値を入れるわけなので、Layoutを変更したらコードの方も変更が必要に。

そこで使えるのが、NSLayoutConstraint.activeプロパティ。(iOS8以上)
Discussionには以下のようにあります。
You can activate or deactivate a constraint by changing this property. Note that only active constraints affect the calculated layout. If you try to activate a constraint whose items have no common ancestor, an exception is thrown. For newly created constraints, the active property is NO by default.

Activating or deactivating the constraint calls addConstraint: and removeConstraint: on the view that is the closest common ancestor of the items managed by this constraint. Use this property instead of calling addConstraint: or removeConstraint: directly.

WWDCでもこれを使うようにオススメされていました。

モノとしては、active が true だと有効、false だと無効 という単純なもの。

Storyboard上で使う可能性があるNSLayoutConstraintを全て定義します。
たとえば、Viewの高さが 0px と、20pxと切り替えたい場合にはこの2つを用意。
このままだとStoryboard上でConflictするので、片方の制約の "Installed" をOffにします。
この2つの制約をViewControllerに紐付けて、タイミングに合わせてactiveの値を変えればOK。
px指定していないView...画面サイズの1/2 とか、Text文字の高さによって変わるとか...も数多くあるはずなのでこれで解決。

...ガ!

active の変更は、viewDidLoad で行っても反映されない罠(?)があります。
(constant の変更は、viewDidLoadで大丈夫。loadViewだって大丈夫。)

ViewControllerのLoad時のイベント発生の順序は
loadView
viewDidLoad
updateViewConstraints
traitCollectionDidChange
viewWillLayoutSubviews
viewDidLayoutSubviews
(※1)
viewWillAppear
(※1)
viewDidAppear

※1 レイアウトによって、viewWillLayoutSubviews, viewDidLayoutSubviews が繰り返しcallされる
ですが、activeはviewDidLayoutSubviews 以降ののタイミングでないとうまく反映されません。
viewDidAppearは表示が終わった後なのでタイミングが遅すぎる。

activeの切り替えはtraitが変わった際(端末が縦、横に回転した時など)にも使うのもなので、traitCollectionDidChange 以降で設定だ というのは納得できるのですが、viewDidLayoutSubviewsは、何回もcallされるメソッドなのであまり独自の処理をいれたくないという気持ちがある。
traitCollectionDidChange で設定できればよかったんだけどなー!

そもそも、画面をLoadする時に変更するのではなく、Storyboard上で起動時の設定にすでにしておくのがいいんですが...
そうすると、Storyboard上でLayoutしにくくなったりするんですよ。
たとえば、最初は全部隠れているけど、アニメーションで動かして画面が出てくることをしたいとか。
うーん、悩ましい。
• • •