swiftuidevice-orientationscreen-rotation

Locking rotation to portrait on SwiftUI without the use of AppDelegate or SceneDelegate


I'm trying to lock the rotation of my application using a bunch of methods that apple have now deprecated over the years and I'm at a loss.

There isn't a SceneDelegate or AppDelegate in Xcode projects anymore using SwiftUI meaning that I can't use any of the previous methods people came up with and programming the solution in SwiftUI: How do I lock a particular View in Portrait mode whilst allowing others to change orientation?. When I try to implement this solution into my program, Xcode returns a warning BUG IN CLIENT OF UIKIT: Setting UIDevice.orientation is not supported. Please use UIWindowScene.requestGeometryUpdate(_:)

Here is my current code

app.swift

import SwiftUI

@main
struct testApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            //Main code here
        }
                
    }
    
}

mainView.swift

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        ZStack {
            // rest of code is here
        }
    }.onAppear {
        UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") // Forcing the rotation to portrait
        AppDelegate.orientationLock = .portrait // And making sure it stays that way
            
    }
        
}

class AppDelegate: NSObject, UIApplicationDelegate {
        
    static var orientationLock = UIInterfaceOrientationMask.all

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return AppDelegate.orientationLock
    }
}

Solution

  • As the warning message suggested, you can have a view request a particular orientation by digging out the UIWindowScene and requesting a geometry update (requires iOS 16).

    For example, this can be done in .onAppear:

    .onAppear {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            scene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in
                // Handle denial of request.
            }
        }
    }
    

    The orientation will stay that way, even when leaving the view. So to have it go back to the current physical orientation, you can add an .onDisappear callback too:

    .onDisappear {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            scene.requestGeometryUpdate(.iOS(interfaceOrientations: .all))
        }
    }
    

    If the user switches the physical device orientation after the view has appeared, then the physical orientation takes effect. To override this, you can use a GeometryReader to measure the screen size and add an .onChange callback to respond to changes:

    private func requestOrientations(_ orientations: UIInterfaceOrientationMask) {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations)) { error in
                // Handle denial of request.
            }
        }
    }
    
    var body: some View {
        GeometryReader { proxy in
            MyPortraitView()
                .onAppear {
                    requestOrientations(.portrait)
                }
                .onChange(of: proxy.size) {
                    requestOrientations(.portrait)
                }
                .onDisppear {
                    requestOrientations(.all)
                }
        }
    }
    

    Note that changes to the view orientation are animated, so if .onChange is used to override a change to the physical orientation, the user experience is not that great.