SwiftUI: How do I lock a particular View in Portrait mode whilst allowing others to change orientation?

Viewed 7415

Environment: SwiftUI using Swift 5.3
Scenario: The default orientation is Portrait, LandscapeLeft & LandscapeRight per Xcode General Setting.
This allows the possibility to have landscape on demand versus having the Xcode Setting to Portrait only.
The project is using SwiftUI Lifecycle vs AppDelegate.

Goal: To have ONLY particular Views able to rotate to landscape; the majority locked in portrait.

Current Modus Operandi: The device is set for Portrait-Only mode within the current View's .upAppear{} and via onReceive{} via the device Orientation-Change Notification.

I found this the only way to actually do a momentary Portrait lock, allowing others to render for landscape.

Problem: The Orientation-Change Notification happens TOO LATE: I see the actual landscape being corrected in real time - so the image snaps back during the rotate.

Question: How to I lock a specific swiftUI View in Portrait mode, allowing others to freely change orientation?

import SwiftUI
import UIKit

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue
            NavigationView {
                Text("Hello, world!")
                    .padding()
                    .navigationTitle("Turkey Gizzards")
                    .navigationBarTitleDisplayMode(.inline)
            }
        }.onAppear {
            UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
        }.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
            UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
        }
    }
}
2 Answers

There is no native SwiftUI method for doing that. It looks like for now, it is mandatory to use the AppDelegate adaptor.

Inside your App main, add this:

@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

Then add this class:

class AppDelegate: NSObject, UIApplicationDelegate {
        
    static var orientationLock = UIInterfaceOrientationMask.all //By default you want all your views to rotate freely

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

And in the specific view designed to be locked in portrait mode:

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue
            NavigationView {
                Text("Hello, world!")
                    .padding()
                    .navigationTitle("Turkey Gizzards")
                    .navigationBarTitleDisplayMode(.inline)
            }
        }.onAppear {
            UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") // Forcing the rotation to portrait
            AppDelegate.orientationLock = .portrait // And making sure it stays that way
        }.onDisappear {
            AppDelegate.orientationLock = .all // Unlocking the rotation when leaving the view
        }
    }
}

You may also, depending on your needs, add another UIDevice.current.setValue(UIInterfaceOrientation.yourOrientation.rawValue, forKey: "orientation") inside the onDisappear to force the rotation when leaving the view.

Are you using UIHostingController? Another workaround might be subclassing it to implement supportedInterfaceOrientations / shouldAutorotate as we might normally do in UIKit:

class HostingController<Content>: UIHostingController<Content> where Content: View {}
Related