目次

目次

【Swift】iOS 16で画面回転禁止処理を行っている箇所で無限ループが発生しクラッシュする

アバター画像
深山侑花
アバター画像
深山侑花
最終更新日2023/03/30 投稿日2023/03/30

はじめに

こんにちは、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で指定されている回転制御内容を取得し、制御内容を自身に反映させることで、各画面ごとの回転制御を行っています。

発生したクラッシュ

今回私が遭遇したのは、タブバークラスでの次のエラーによるクラッシュです。

crash.png

前項で紹介した手法での画面回転制御時に、iOS 16環境でのみ EXC_BAD_ACCESSでクラッシュする事象が発生しました。

こちらは、Appレビューのダイアログを表示したときに発生したエラーになります。

このエラー状況を調査をしてみたところ、ダイアログ表示時に、該当箇所が繰り返し呼び出され続けていることがわかりました。

原因と解決方法

エラー箇所にある最前面View取得処理の独自メソッド topViewController()内に問題があると考え、Appレビューのダイアログが表示時にtopViewController()で返却されているUIViewControllerがなにかを調べてみたところ、SKStoreReviewViewControllerというクラスであることがわかりました。

topViewController_response.png

これはAppleが公開している UIViewControllerではく、Appleが提供するStoreKitフレームワークの非公開なクラスのようです。 このことから、 SKStoreReviewViewController内のsupportedInterfaceOrientationsを返却する処理と今回発生したクラッシュがなにか関係がありそうだということがわかりました。

topViewController()にて、Appレビューのダイアログに該当するSKStoreReviewViewControllerが返却された際は、そのViewのsupportedInterfaceOrientationsを使用しないようにすることで、処理が繰り返し呼び出されるのを防げるのではないかと考えました。

しかし、 SKStoreReviewViewControllerは非公開なクラスであるため、以下のようにisを使ってクラスの等価性を比較することはできません。

solution_error.png

そこで、別の解決方法を検討したところ、以前 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したフレームワーク内のクラスのような、内部でどのような処理が行われているかわからないクラスを参照する可能性を考慮して適切にハンドリングをしましょう。

参考文献

アバター画像

深山侑花

目次