SwiftUI - Device orientation

Hey ;-),

my app supports the portrait mode at the moment. And I want to implement the landscape mode as well.

2 questions regarding device orientation detection:

  • What is the best way to detect the orientation of a device? A solution that is working on app launch too (not only on a rotation of the device)

At the moment I'm testing:

@Environment(\.verticalSizeClass) private var verticalSizeClass

But it seems that the devices I'm using for tests (simulators and a real device) do not support upside down mode. In the deployment info of my app I choose all 4 orientations (portrait, upside down, landscape left and right). Still the upside down does not redrawn the app and the orientation detected by "verticalSizeClass" says: compact

  • Is this a normal behavior (ignoring upside down mode) for all devices?

Thanks in advance, Jackson

Answered by DTS Engineer in 799764022

To add to the suggestion @Jackson-G

  • To specify a supportedOrientations, you could would need to use a UIKit app to create a fixed-orientation UINavigationController, then interface with SwiftUI via UIViewControllerRepresentable.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let navigationController = SupportedOrientationHostingController(rootView: ContentView())
            window.rootViewController = navigationController
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

struct SupportedOrientationsPreferenceKey: PreferenceKey {
    static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
    
    static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
        value = nextValue()
    }
}

final class SupportedOrientationHostingController<Content: View>: UIHostingController<SupportedOrientationHostingController.Root<Content>> {
    var orientations: Box!
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        orientations.supportedOrientations
    }
    
    init(device: UIDevice = .current, rootView: Content) {
        let box = Box(device: device)
        let orientationRoot = Root(contentView: rootView, box: box)
        super.init(rootView: orientationRoot)
        self.orientations = box
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    final class Box {
        var supportedOrientations: UIInterfaceOrientationMask
        init(device: UIDevice) {
            self.supportedOrientations = device.userInterfaceIdiom == .pad ? .all : .allButUpsideDown
        }
    }
    
    struct Root<Content: View>: View {
        @State private var currentOrientation: UIDeviceOrientation = UIDevice.current.orientation
        private let orientationPublisher = NotificationCenter.Publisher(
            center: NotificationCenter.default,
            name: UIDevice.orientationDidChangeNotification
        )
        let contentView: Content
        let box: Box
        
        var body: some View {
            contentView
                .onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
                    self.box.supportedOrientations = $0
                }
                .onReceive(orientationPublisher) { _ in
                    self.currentOrientation = UIDevice.current.orientation
                }
                .environment(\.deviceOrientation, currentOrientation)
        }
    }
}

extension View {
    /// Sets the supported interface orientations for this view and its ancestors.
    /// - Parameters:
    ///   - orientations: The interface orientations that the view supports.
    ///   - sizeClasses: The user interface size classes that this view uses to determine layout.
    /// - Returns: A view that supports the specified interface orientations.
    func supportedOrientations(_ orientations: UIInterfaceOrientationMask, sizeClasses: [UserInterfaceSizeClass?] = .all) -> some View {
        // Propagate the requested orientations up the view hierarchy:
        preference(key: SupportedOrientationsPreferenceKey.self, value: orientations)
    }
}

extension Collection where Element == UserInterfaceSizeClass? {
    static var all: [UserInterfaceSizeClass?] {
        [.compact, .regular]
    }
}


To also observe the orientations changes, you could define an EnvironmentValues such that,



struct DeviceOrientationKey: EnvironmentKey {
    static let defaultValue: UIDeviceOrientation = .unknown
}

extension EnvironmentValues {
    var deviceOrientation: UIDeviceOrientation {
        get { self[DeviceOrientationKey.self] }
        set { self[DeviceOrientationKey.self] = newValue }
    }
}

struct ContentView: View {
    @Environment(\.deviceOrientation) private var deviceOrientation
    var body: some View {
        Text("Hello, world!")
            .onChange(of: deviceOrientation) {
                dump(deviceOrientation)
            }
            .supportedOrientations(.landscape)
    }
}

iPhones generally default to not supporting upside down with iPads supporting all orientations.

I'm not familiar with other platform support.

Ah good to know, that iPhones did not support upside down modes. Thanks @jlilest

And I just tested an iPad. There all orientations work, but "verticalSizeClass" shows "regular" for all 4 modes. It seems that "verticalSizeClass" doesn't work for iPhone and iPad the same way

Or do I miss something?

You can enable upside down for the phone, but I think the default was done because laying on your back while holding the phone above you can easily look to the phone like upside down. Apple created the size categories years ago and never updated them.

I think the Apple intent is you layout your content to fit the screen and not really think of it as orientation.

On the ipad the user can have two apps open and one might be the size of a phone. The stage manager increases the possible sizes.

To add to the suggestion @Jackson-G

  • To specify a supportedOrientations, you could would need to use a UIKit app to create a fixed-orientation UINavigationController, then interface with SwiftUI via UIViewControllerRepresentable.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let navigationController = SupportedOrientationHostingController(rootView: ContentView())
            window.rootViewController = navigationController
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

struct SupportedOrientationsPreferenceKey: PreferenceKey {
    static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
    
    static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
        value = nextValue()
    }
}

final class SupportedOrientationHostingController<Content: View>: UIHostingController<SupportedOrientationHostingController.Root<Content>> {
    var orientations: Box!
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        orientations.supportedOrientations
    }
    
    init(device: UIDevice = .current, rootView: Content) {
        let box = Box(device: device)
        let orientationRoot = Root(contentView: rootView, box: box)
        super.init(rootView: orientationRoot)
        self.orientations = box
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    final class Box {
        var supportedOrientations: UIInterfaceOrientationMask
        init(device: UIDevice) {
            self.supportedOrientations = device.userInterfaceIdiom == .pad ? .all : .allButUpsideDown
        }
    }
    
    struct Root<Content: View>: View {
        @State private var currentOrientation: UIDeviceOrientation = UIDevice.current.orientation
        private let orientationPublisher = NotificationCenter.Publisher(
            center: NotificationCenter.default,
            name: UIDevice.orientationDidChangeNotification
        )
        let contentView: Content
        let box: Box
        
        var body: some View {
            contentView
                .onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
                    self.box.supportedOrientations = $0
                }
                .onReceive(orientationPublisher) { _ in
                    self.currentOrientation = UIDevice.current.orientation
                }
                .environment(\.deviceOrientation, currentOrientation)
        }
    }
}

extension View {
    /// Sets the supported interface orientations for this view and its ancestors.
    /// - Parameters:
    ///   - orientations: The interface orientations that the view supports.
    ///   - sizeClasses: The user interface size classes that this view uses to determine layout.
    /// - Returns: A view that supports the specified interface orientations.
    func supportedOrientations(_ orientations: UIInterfaceOrientationMask, sizeClasses: [UserInterfaceSizeClass?] = .all) -> some View {
        // Propagate the requested orientations up the view hierarchy:
        preference(key: SupportedOrientationsPreferenceKey.self, value: orientations)
    }
}

extension Collection where Element == UserInterfaceSizeClass? {
    static var all: [UserInterfaceSizeClass?] {
        [.compact, .regular]
    }
}


To also observe the orientations changes, you could define an EnvironmentValues such that,



struct DeviceOrientationKey: EnvironmentKey {
    static let defaultValue: UIDeviceOrientation = .unknown
}

extension EnvironmentValues {
    var deviceOrientation: UIDeviceOrientation {
        get { self[DeviceOrientationKey.self] }
        set { self[DeviceOrientationKey.self] = newValue }
    }
}

struct ContentView: View {
    @Environment(\.deviceOrientation) private var deviceOrientation
    var body: some View {
        Text("Hello, world!")
            .onChange(of: deviceOrientation) {
                dump(deviceOrientation)
            }
            .supportedOrientations(.landscape)
    }
}
SwiftUI - Device orientation
 
 
Q