iOS 18でのタブ切り替え時におけるライフサイクル遅延の原因と対策
Xcode 16でビルドしたアプリをiOS 18.0以降のデバイスで実行すると、UITabBarController
を用いた画面遷移(タブ切り替え)において、従来の挙動と異なる動作を確認したので調査した。
概要
- タブバーのタブ切り替えで、iOS 18からクロスディゾルブ(フェードイン・フェードアウト)アニメーションが適用されるようになった
- その影響で、「隠れる画面の
viewDidDisappear
」と「表示される画面のviewDidAppear
」の呼び出しタイミングが0.5〜0.7秒遅延する - 既存のライフサイクルに依存した実装がある場合、不具合が発生する可能性がある
この変更は、Appleの公式ドキュメントに明記されていないため、開発者が気づかずに問題に遭遇する可能性がある。
問題の背景
開発中のアプリで、iOS 18にアップデートしたユーザーから複数の問題が報告されていた。
問題の内容は、特定の画面遷移やタブ切り替え時に意図しない挙動が発生するというものだった。該当箇所の実装は変更していないにもかかわらず、挙動が変わったように見えたため、ライフサイクルの変化が原因ではないかと疑っていた。
同様の疑問を持つ開発者もいるようで、StackOverflowやApple Developer Forumsでも関連する質問がいくつか投稿されていた。しかし、有益な回答はついておらず、Appleの公式ドキュメントにも該当する変更点の記載はなかった。
そんな中、anzさんの以下のTwitter(現X)投稿を発見した。
この投稿をきっかけに、問題の原因を特定するための検証を行った。
検証内容
UITabBarControllerを用意し、画面1と画面2を切り替えるだけの簡単なサンプルプロジェクトを作成した。それぞれの画面にライフサイクルメソッドをオーバーライドし、ログを仕込んで挙動を確認した。
以下は、画面1(赤色)のコード例である。
final class Tab1ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
title = "タブ1"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NSLog("🟥Tab1 - viewWillAppear")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NSLog("🟥Tab1 - viewDidAppear")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NSLog("🟥Tab1 - viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NSLog("🟥Tab1 - viewDidDisappear")
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
NSLog("🟥Tab1 - viewWillLayoutSubviews")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
NSLog("🟥Tab1 - viewDidLayoutSubviews")
}
}
ライフサイクルログの比較
iOS 17とiOS 18でタブ切り替え時のライフサイクルのログを比較した結果、以下のような違いが確認された。
iOS 18.2 | iOS 17.5 | |
---|---|---|
🟢Tab2 - viewWillAppear | 🟢Tab2 - viewWillAppear | 変更なし |
🟥Tab1 - viewWillDisappear | 🟥Tab1 - viewWillDisappear | 変更なし |
🟢Tab2 - viewWillLayoutSubviews | 🟢Tab2 - viewWillLayoutSubviews | 変更なし |
🟢Tab2 - viewDidLayoutSubviews | 🟢Tab2 - viewDidLayoutSubviews | 変更なし |
(0.5〜0.7秒遅延) | 🟥Tab1 - viewDidDisappear | iOS 17.5では間隔を空けずに呼ばれる |
🟢Tab2 - viewDidAppear | 🟢Tab2 - viewDidAppear | |
🟥Tab1 - viewDidDisappear |
iOS 17とiOS 18の違いについて調査
この差異の原因を調査するため、iOSシミュレータの [Debug] → [Slow Animations] を有効にしてトランジションを観察したところ、iOS 18ではタブ切り替え時にクロスディゾルブ(フェードイン・フェードアウト)アニメーションが適用されていることがわかった。
このアニメーションが原因で、ライフサイクルメソッドの呼び出しタイミングが遅延していると考えられる。
アニメーションを無効化する
ライフサイクルの遅延を回避するため、タブ切り替え時のアニメーションを無効化する方法を検討した。以下のように、UITabBarController
を継承したカスタムクラスを作成し、アニメーションを0秒に設定することで、実質的に無効化できる。
final class MyTabController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
NSLog("🟦TabBarController - viewDidLoad")
delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard
let fromView = selectedViewController?.view,
let toView = viewController.view,
fromView != toView
else {
return false
}
UIView.transition(
from: fromView,
to: toView,
duration: 0,
options: [],
completion: nil
)
return true
}
}
実装時の注意点
この方法は、あくまで一時的な回避策である。アニメーションを無効化することで、ユーザー体験が損なわれる可能性があるため、以下の点に注意する必要がある。
- アプリ全体のデザインポリシーに影響を与えないか確認する
- ライフサイクルのタイミングに依存しない実装を検討する
しかし、将来的なiOSのアップデートを考慮すると、タブ切り替え時のアニメーションを無効化する対応は推奨できない。
結論と次のステップ
以上の検証から、iOS 18ではタブ切り替え時にクロスディゾルブアニメーションが導入され、それに伴いライフサイクルメソッドの呼び出しタイミングが遅延することがわかった。この変更は公式ドキュメントに明記されていないため、既存のライフサイクルに依存した実装がある場合、不具合が発生する可能性がある。
もし、タブ切り替え時に謎の不具合が発生している場合は、iOS 17とiOS 18でライフサイクルログを比較し、可能であれば、アニメーションの有無に関わらず動作するように、画面側の実装を改めた方が良いだろう。