画面の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
の値を取得できるようにする為、SceneDelete
をObservableObject
に適合させておきます。
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
を使用できるようにするsupportedInterfaceOrientationsFor
でOrientationController
のcurrentOrientation
を返すようにする
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
あとは、画面の向きを制御をしたいView
のonAppear
とonDisapper
で、画面方向のロックやアンロックをするだけです。
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
を使用しよう
おわりに
画面向きの制御をする為だけにAppDelegate
やSceneDelegate
を呼ぶのなぜか悔しいので、その他の方法で制御できる方法があれば教えていただきたいです。
その他、より良い実装あれば優しく教えていただけると嬉しいです。
SwiftUIで簡単に画面の向き制御できないのはなぜだろう?そもそもSwiftUIなんだから全画面に対応してよねという方針なんだろうか?