SwiftUIで画面の向きを制御する方法

2023.08.25

画面のPortrait固定をしているアプリである特定の画面の時だけは全画面向きに対応できるようにしたかったので、SwiftUIで画面の向きを制御する方法を調べました。

環境

  • Xcode 14.2
  • iOS 16.2

はじめに

「SwiftUIで」と言っておきながら、UIKitの力を借ります。

SwiftUIで画面の向きを変更する方法

setValueを使用する方法

調べてみるとUIDevice.current.setValueに画面向きの値を入れることで、その画面向きになるという方法は出てくるのですが、

 UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue,
                           forKey: "orientation")

この方法はiOS 16からは動作してくれません、、。今回はiOS 16以上をターゲットにしている為、別の方法を行います。

setNeedsUpdateOfSupportedInterfaceOrientationsを使用する

iOS 16から使用できるsetNeedsUpdateOfSupportedInterfaceOrientations()を使うことで画面の向きを更新することが出来ます。

サポートされているインターフェイスの方向またはプレゼンテーションに優先されるインターフェイスの方向の変更についてViewControllerに通知します。

ただこのメソッドは、UIViewControllerのメソッドになる為、SwiftUIで実行する為には一工夫が必要です。

SwiftUIで画面の向きを変更するコード

OrientationController

画面の向きを制御するクラスを作成しました。

class OrientationController {

    private init() {}

    static let shared = OrientationController()

    var currentOrientation: UIInterfaceOrientationMask = .portrait

    // 画面向き制御のアンロック
    func unlockOrientation() {
        currentOrientation = .all
    }

    // 画面を指定した向きでロック
    func lockOrientation(to orientation: UIInterfaceOrientationMask, onWindow window: UIWindow) {

        currentOrientation = orientation

        guard var topController = window.rootViewController else {
            return
        }
        // 最前面のViewControllerを取得
        while let presentedViewController = topController.presentedViewController {
            topController = presentedViewController
        }
        // 端末の画面の向きをcurrentOrientationの値で更新
        topController.setNeedsUpdateOfSupportedInterfaceOrientations()
    }
}

前述したようにsetNeedsUpdateOfSupportedInterfaceOrientationsを使用する為には、UIViewControllerを取得する必要がある為、lockOrientation(to:, onWindow:)ではUIWIndowを引数として渡すようにしています。

SceneDelegate

Environmentから現在のWindowの値を取得できるようにする為、SceneDeleteObservableObjectに適合させておきます。

class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }
        self.window = windowScene.keyWindow
    }
}

AppDelegate

App内で@UIApplicationDelegateAdaptorを使用して、AppDelegateを使用できるようにします。

@main
struct SampleApp: App {

    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

AppDelegate内では、

  • SceneDelegateを使用できるようにする
  • supportedInterfaceOrientationsForOrientationControllercurrentOrientationを返すようにする
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
        if connectingSceneSession.role == .windowApplication {
            configuration.delegateClass = SceneDelegate.self
        }
        return configuration
    }

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return OrientationController.shared.currentOrientation
    }
}

対象のView

あとは、画面の向きを制御をしたいViewonAppearonDisapperで、画面方向のロックやアンロックをするだけです。

struct FreeOrientationView: View {

    @EnvironmentObject var sceneDelegate: SceneDelegate

    var body: some View {
        Text("I am free")
            .padding()
            .onAppear {
                // 画面の向き制御をアンロック
                OrientationController.shared.unlockOrientation()
            }
            .onDisappear {
                if let window = SceneDelegate.window {
                    // 画面の向きを.portraitでロック
                    OrientationController.shared.lockOrientation(to: .portrait,
                                                                 onWindow: window)
                }
            }
    }
}

これである特定の画面では画面の向きの制限を無くすことができ、閉じる時には画面の向きを固定に戻すことが出来ました。

まとめ

  • iOS 16からは画面の向きを更新したい場合には、setNeedsUpdateOfSupportedInterfaceOrientationsを使用しよう

おわりに

画面向きの制御をする為だけにAppDelegateSceneDelegateを呼ぶのなぜか悔しいので、その他の方法で制御できる方法があれば教えていただきたいです。 その他、より良い実装あれば優しく教えていただけると嬉しいです。

SwiftUIで簡単に画面の向き制御できないのはなぜだろう?そもそもSwiftUIなんだから全画面に対応してよねという方針なんだろうか?

参考