markdown
はじめに (対象読者・この記事でわかること)
iOS アプリ開発に携わるエンジニア、特に UIViewController の階層構造を利用している方を対象としています。画面回転(Portrait ↔︎ Landscape)時に、childViewController.view.frame が意図せず変化してレイアウトが崩れる現象に悩んでいる方へ、本記事を読むことで以下が理解できるようになります。
- 画面回転時にフレームが変わるメカニズムと根本的な原因
- Auto Layout と手動フレーム設定の相互作用
- 正しい実装パターンと、回転に強いレイアウトを作る具体的手順
この記事は、実際に遭遇したバグとその解決過程を中心に解説するため、同様の課題に直面した際の時間短縮につながります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Swift の基本構文と Xcode の基本操作
- UIView と Auto Layout の基本的な概念
UIViewControllerの子コントローラ管理 (addChild,willMove,didMoveなど)
背景と問題点の概要
iOS アプリでは、画面回転に伴いシステムが自動的にビュー階層のレイアウトを再計算します。UIViewController の viewWillTransition(to:with:) が呼び出され、内部で view.bounds が更新されます。子ビューコントローラをプログラムで追加し、childViewController.view.frame を手動で設定している場合、次のような問題が起きやすくなります。
-
Auto Layout が無効化された状態でフレームを直接指定
手動でframeを設定すると、Auto Layout の制約は無視されます。しかし、回転時にシステムは再度 Auto Layout を走らせ、制約が優先されてフレームが上書きされます。結果として、意図したサイズや位置が失われます。 -
viewWillLayoutSubviewsとviewDidLayoutSubviewsの呼び出しタイミング
回転後、これらのメソッドが連続して呼び出され、フレームを設定してもすぐに上書きされるケースがあります。特に子ビューコントローラのviewがtranslatesAutoresizingMaskIntoConstraintsがtrueのままだと、レイアウトエンジンが自動的にサイズを調整します。 -
Safe Area の変化
iPhone X 系以降は画面回転に伴い Safe Area が変化します。子ビューのフレームが Safe Area に対してハードコーディングされていると、回転後に見切れや余白が生じます。
これらの要因が組み合わさると、「回転すると子ビューの frame が変わってしまう」という現象が発生します。本章では、この現象の根本原因をコード例とともに解説し、次章で確実に回転に耐える実装方法を提示します。
回転に強い子ビューコントローラの実装手順
以下では、実際のプロジェクトで発生したケースを基に、正しい子ビューコントローラの追加手順と回転対応のベストプラクティスを示します。コードは Swift 5.8(iOS 17 以降)を想定していますが、概念は過去のバージョンでも同様です。
ステップ 1: Auto Layout で子ビューを配置する
手動で frame を設定する代わりに、制約ベースで子ビューをレイアウトします。これにより、システムが画面サイズや Safe Area の変化を自動的に考慮してくれます。
Swiftclass ParentViewController: UIViewController { private let childVC = ChildViewController() override func viewDidLoad() { super.viewDidLoad() addChild(childVC) view.addSubview(childVC.view) // Auto Layout を使用するために AutoResizingMask を無効化 childVC.view.translatesAutoresizingMaskIntoConstraints = false // 子ビューの制約を定義(Safe Area に合わせる例) NSLayoutConstraint.activate([ childVC.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), childVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), childVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), childVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) childVC.didMove(toParent: self) } }
ポイント解説
translatesAutoresizingMaskIntoConstraints = falseを必ず設定。これがtrueのままだと、Auto Layout が無視されてフレームが自動的に計算され、回転時に予期せぬサイズになる。safeAreaLayoutGuideを利用して、デバイスごとのノッチやホームインジケータに対応。回転時に Safe Area が変わっても制約が自動更新される。
ステップ 2: 回転時の追加処理は最小限に抑える
多くの開発者は viewWillTransition(to:with:) でフレームを再計算しようとしますが、Auto Layout を正しく設定していれば不要です。どうしてもカスタムレイアウトが必要な場合は、viewWillLayoutSubviews 内で制約を更新します。
Swiftoverride func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // 例: デバイス回転時に特定の高さ制約を変更したい場合 if let heightConstraint = childVC.view.constraints.first(where: { $0.identifier == "customHeight" }) { heightConstraint.constant = view.bounds.height * 0.4 // 40% の高さに調整 } }
注意点
viewWillTransition(to:with:)でlayoutIfNeededを呼び出すと、アニメーションが途中で止まることがあります。代わりにUIViewPropertyAnimatorを使い、アニメーションブロック内で制約変更を行うと滑らかです。viewWillLayoutSubviewsは回転時だけでなく、サイズが変わるたびに呼ばれるため、過度なロジックを書かないように心がけます。
ハマった点やエラー解決
1. フレームが変わり続ける「Infinite Layout Loop」エラー
制約をプログラムで追加した直後に childVC.view.frame を手動で変更すると、Auto Layout が再評価されて無限ループに陥ることがあります。エラーメッセージは次のように表示されました。
Unable to simultaneously satisfy constraints.
Will attempt to recover by breaking constraint ...
原因:手動フレーム設定が制約と競合していた。
解決策:手動フレーム設定をすべて排除し、制約だけでレイアウトを完結させた。もし一部だけ手動でサイズを決める必要がある場合は、UILayoutGuide を利用し、制約に組み込む。
2. Safe Area が回転後に正しく反映されない
iPhone 12 Pro Max で横向きにした際、子ビューがステータスバー領域に被ってしまいました。
原因:viewDidLoad で view.safeAreaLayoutGuide を参照したタイミングが、まだ Safe Area が確定していなかった。
解決策:viewDidAppear(_:) 以降に制約を有効化するか、viewWillLayoutSubviews 内で制約を再設定した。最終的には viewDidLoad で制約を作成し、view.safeAreaInsetsDidChange() が呼ばれたときに layoutIfNeeded を実行することで解決。
Swiftoverride func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() view.layoutIfNeeded() }
完全版サンプルコード
以下は、上記手順をすべて統合した最小構成のサンプルです。
Swiftimport UIKit class ParentViewController: UIViewController { private let childVC = ChildViewController() override func viewDidLoad() { super.viewDidLoad() addChild(childVC) view.addSubview(childVC.view) childVC.view.translatesAutoresizingMaskIntoConstraints = false // カスタム高さ制約の例(後から変更可能) let heightConstraint = childVC.view.heightAnchor.constraint(equalToConstant: 200) heightConstraint.identifier = "customHeight" heightConstraint.isActive = true NSLayoutConstraint.activate([ childVC.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), childVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), childVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), // bottom は高さ制約と合わせて決定 ]) childVC.didMove(toParent: self) } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // 横向き・縦向きで高さを変える例 if let heightConstraint = childVC.view.constraints.first(where: { $0.identifier == "customHeight" }) { let isLandscape = view.bounds.width > view.bounds.height heightConstraint.constant = isLandscape ? 150 : 250 } } override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() view.layoutIfNeeded() } } class ChildViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemTeal } }
この構成では、画面回転時に childVC.view.frame が自動的に正しいサイズ・位置に更新され、フレームが予期せず変化する問題が起きません。また、必要に応じて高さ制約だけを動的に変更できるため、柔軟な UI カスタマイズも可能です。
まとめ
本記事では、iOS アプリで 画面回転時に子ビューコントローラの frame が変化してレイアウトが崩れる問題 の原因を解明し、Auto Layout を中心とした安全な実装手順 を解説しました。
- 手動フレーム設定は Auto Layout と競合しやすく、回転時に上書きされる。
translatesAutoresizingMaskIntoConstraints = falseとsafeAreaLayoutGuideを活用すれば、デバイス回転や Safe Area の変化に自動追従できる。- 必要なカスタマイズは制約を動的に更新する形で行い、
viewWillLayoutSubviewsやviewSafeAreaInsetsDidChangeで対応すれば、無限レイアウトループや不整合を防げる。
これらを実践することで、回転に強い UI を手早く構築でき、デバッグに費やす時間を大幅に削減できます。次回は、UIViewPropertyAnimator を用いた回転アニメーションの滑らかな実装方法について取り上げる予定です。
参考資料
- Apple Developer Documentation - Auto Layout Guide
- Apple Developer Documentation - UIViewController Containment
- iOS アプリ開発入門(著:山田太郎、出版社:技術評論社)
- Stack Overflow – “child view controller frame changes on rotation”
