soranoba
soranoba Author of soranoba.net
programming

xib上でhiddenに設定したconstraintsのactivateが正しく機能しない問題への対応

iOSアプリ開発において, xib上でPortrait用・Landscape用のconstraintsをそれぞれ設定し, コードでactivate/deactivateを切り替えることが往々にしてあると思います.
activate/deactivateの処理が正しく機能しなくなる現象に遭遇したので, その対応方法についてのメモです.

isActivateを操作しても巻き戻るケース

条件は明確です.

  • XCode11.5 (XCode11.x 全ての可能性が高いです. 10.xで起きていたかは不明です)
  • xib上でHiddenに設定したNSLayoutConstraintであること
  • そのNSLayoutConstraintviewWillLayoutSubviewsからviewDidLayoutSubviewsの間に操作すること

これらの条件を満たすことで, isActivateに代入した値が反映されず, 巻き戻る現象が発生します.
これは, 例えば以下のようなコードの場合が該当します.

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    if UIApplication.shared.statusBarOrientation.isPortrait {
        NSLayoutConstraint.activate(portraitConstraints)
        NSLayoutConstraint.deactivate(landscapeConstraints)
    } else {
        NSLayoutConstraint.activate(landscapeConstraints)
        NSLayoutConstraint.deactivate(portraitConstraints)
    }
}

対策方法

1. 回転時の対応

回転の場合のみであれば, viewWillTransition(to:with:)を使うという方法があります.

override func updateViewConstraints() {
    if UIApplication.shared.statusBarOrientation.isPortrait {
        NSLayoutConstraint.activate(portraitConstraints)
        NSLayoutConstraint.deactivate(landscapeConstraints)
    } else {
        NSLayoutConstraint.activate(landscapeConstraints)
        NSLayoutConstraint.deactivate(portraitConstraints)
    }

    super.updateViewConstraints()
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { _ in
        self.view.setNeedsUpdateConstraints()
    }, completion: nil)
}

余談ですが, viewWillLayoutSubviewssetNeedsUpdateConstraintsを呼び出すのはフィードバックループ (無限ループ) が発生する可能性があるので, 公式のガイドではやってはいけない呼び出しとされています. 1
その為, ここではその方法を取りません.

2. 外観モードが変更された場合

これだけだと外観モードの変更時に壊れるという不具合があります.
外観モードとはiOS13から導入されたOSにあるライトモード・ダークモードの設定のことです.

外観モードが変更され, 諸々の処理が終わった後でないと適用できない為, アドホックな対応が必要となります.

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)
    coordinator.animate(alongsideTransition: { _ in
    }, completion: { _ in
        self.view.setNeedsUpdateConstraints()
    })
}

ここで大切なのは, completionで実行するという点です. alongsideTransition側で実行しても機能しない点に注意が必要です. 謎ですが.

3. ViewController表示時

これでもまだ不足しています.
iOS11.xなどにおいて (iOS13未満だと思われますが, 対象のOSについては特に調査していません) ViewController表示時のconstraintsが壊れるという現象があります.
これについてはviewDidAppear(_:)を用いることで解消することができます.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    view.setNeedsUpdateConstraints()
}

根本的に回避する方法

恐らくこれら全ての対応を行うことで解消されると思われるものの, 将来的なバージョンにおいて挙動がまた変わる可能性があるので根本的に対応するのであれば, xib上でhiddenにしないという方法を推奨します.
とはいえ, このようなconstraintsになるのは得てして複雑な場合なので難しいのですが.

なお、記事中のサンプルコードはGitHub上に公開しています.


(Updated: )

comments powered by Disqus