はじめに
こんにちは、iOSアプリ開発グループの深山です。
私が担当しているアプリでは、画面の回転制御を行うために、最前面のViewを取得する処理が行われています。あるとき、開発中にその処理が無限ループする不具合に遭遇しました。
この記事ではその事象内容と解決方法を紹介します。
動作環境
- Xcode 14.1
- iOS 16.1.2
- Swift 5.7.1
最前面のUIViewControllerの取得
私の担当アプリでは、デザインの関係上、一部の画面でのみ画面回転を許可しています。
画面回転時に参照される
supportedInterfaceOrientationsをオーバーライドすることで、この制御を行っています。
このアプリでは
UITabBarControllerを使用しています。
画面回転制御を行うために、回転させるかどうかをルートとなるタブバークラスでは判断せず、以下のように、表示されている各Viewに移譲する形となっています。
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { UIApplication.topViewController()?.supportedInterfaceOrientations ?? .portrait } |
UIApplication.topViewController()は以下のようになっています。(参考)
extension UIApplication { static func topViewController(_ viewController: UIViewController? = shared.keyWindow?.rootViewController) -> UIViewController? { if let navigationController = viewController as? UINavigationController { return topViewController(navigationController.visibleViewController) } if let tabBarController = viewController as? UITabBarController { return topViewController(tabBarController.selectedViewController) } if let presentedViewController = viewController?.presentedViewController { return topViewController(presentedViewController) } return viewController } } |
各Viewクラスでは、 supportedInterfaceOrientationsをオーバーライドし、各画面での画面回転の有無や向きを指定して制御を行います。
// HogeViewController.swift // 例: 回転させたくない場合 -> 画面の向きをportraitで固定 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } |
上記のように、ルートのタブバークラスが最前面のViewで指定されている回転制御内容を取得し、制御内容を自身に反映させることで、各画面ごとの回転制御を行っています。
発生したクラッシュ
今回私が遭遇したのは、タブバークラスでの次のエラーによるクラッシュです。
前項で紹介した手法での画面回転制御時に、iOS 16環境でのみ EXC_BAD_ACCESSでクラッシュする事象が発生しました。
こちらは、Appレビューのダイアログを表示したときに発生したエラーになります。
このエラー状況を調査をしてみたところ、ダイアログ表示時に、該当箇所が繰り返し呼び出され続けていることがわかりました。
原因と解決方法
エラー箇所にある最前面View取得処理の独自メソッド topViewController()内に問題があると考え、Appレビューのダイアログが表示時に topViewController()で返却されている UIViewControllerがなにかを調べてみたところ、 SKStoreReviewViewControllerというクラスであることがわかりました。
これはAppleが公開している
UIViewControllerではく、Appleが提供する
StoreKitフレームワークの非公開なクラスのようです。
このことから、
SKStoreReviewViewController内の
supportedInterfaceOrientationsを返却する処理と今回発生したクラッシュがなにか関係がありそうだということがわかりました。
topViewController()にて、Appレビューのダイアログに該当する SKStoreReviewViewControllerが返却された際は、そのViewの supportedInterfaceOrientationsを使用しないようにすることで、処理が繰り返し呼び出されるのを防げるのではないかと考えました。
しかし、 SKStoreReviewViewControllerは非公開なクラスであるため、以下のように isを使ってクラスの等価性を比較することはできません。
そこで、別の解決方法を検討したところ、以前
UIAlertController表示時にも同様の事象が発生したことを思い出しました。
このことから、
SKStoreReviewViewControllerに限らず、内部でどのような処理が行われているかわからないクラスを参照することがエラーの原因になるのではないかと推測しました。
このことを踏まえ、以下のような対応を行いました。
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { getTopViewController()?.supportedInterfaceOrientations ?? .portrait } func getTopViewController() -> UIViewController? { UIApplication.topViewController().flatMap { Bundle(for: type(of: $0)).bundleIdentifier == Bundle(for: type(of: self)).bundleIdentifier ? $0 : nil } } |
getTopViewController()は、最前面の UIViewControllerを取得する際、特定の UIViewController参照時に nilを返却する対応を含んだメソッドです。
『アプリ内で独自に作成した
UIViewControllerクラス以外 = Apple等が提供するフレームワーク上の
UIViewControllerクラス』
と見なし、こちらを取得したときに
nilを返却することで適切にハンドリングが行えるようにしました。
この対応により、無事クラッシュを起こすことなく、Appレビューのダイアログを表示することができました。
補足
次のように、文字列比較で、任意のクラスに対してハンドリングを行うこともできます。
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { UIApplication.topViewController().flatMap { String(describing: type(of: $0)) == "SKStoreReviewViewController" ? nil : $0 }?.supportedInterfaceOrientations ?? .portrait } |
しかしこの方法だと、対象のクラスが複数ある場合・ハンドリングすべきクラスを新たに発見した場合に、条件文を都度追加していく必要があり、コードの見通しも悪くなります。
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { UIApplication.topViewController().flatMap { String(describing: type(of: $0)) == "SKStoreReviewViewController" || $0 is UIAlertController || .... ? nil : $0 }?.supportedInterfaceOrientations ?? .portrait } |
前項で紹介した実装であれば、今回の事象が起こり得る複数のクラスに対する処理を、簡潔に記述することができるので、そちらを用いるのが良いでしょう。
まとめ
画面回転制御時、独自に作成した
UIViewControllerクラス以外の表示が原因で不具合が発生する可能性があることがわかりました。
supportedInterfaceOrientationsのオーバーライドを用いて画面回転を制御する場合は、importしたフレームワーク内のクラスのような、内部でどのような処理が行われているかわからないクラスを参照する可能性を考慮して適切にハンドリングをしましょう。
参考文献
- 【Swift】最前面のUIViewControllerを取得する方法 – Qiita
- ViewControllerごとに画面の向きを固定する – Qiita
- 【Swift4】特定の画面で回転を制御する方法について、サンプルを作って理解する – Qiita
- Swiftで一部の画面の回転禁止を導入したらiOS9はsupportedInterfaceOrientations was invoked recursivelyと例外が飛んでクラッシュする – Karakuri.com
この記事を書いた人
最近書いた記事
- 2023.03.30【Swift】iOS 16で画面回転禁止処理を行っている箇所で無限ループが発生しクラッシュする
- 2023.03.27SwiftLintでカスタムルールを追加してみた
- 2022.09.30【Swift】CoreDataのユニットテストの環境構築
- 2022.09.30【Swift】CoreDataの概念を知る