Observe interface orientation in SwiftUI

“Apple strongly discourages the use of ‘landscape’ and ‘portrait’ modes in applications and strongly encourages the use of size classes.”

That being said, sometimes we really need to know whether a device is in a landscape or portrait mode or even distinguish between landscape right and landscape left.

The most commonly used method found on the internet involves observing UIDevice.orientationDidChangeNotification. Unfortunately, this method is not entirely accurate. The device’s orientation and the interface orientation are two different things. For instance, the device orientation may include values like .faceUp or .faceDown, which do not translate to interface orientation. Additionally, even if the user enables rotation lock for the device, the device orientation may still change while the interface remains unchanged.

Thus, how can we accurately observe the true interface orientation? Retrieve it from: view.window?.windowScene?.interfaceOrientation

1. UIViewController that notifies about current interface orientation via Combine:

				
					class InterfaceOrientationProviderVC: UIViewController {
    private let interfaceOrientationSubject: CurrentValueSubject<UIInterfaceOrientation, Never>
    
    init(interfaceOrientationSubject: CurrentValueSubject<UIInterfaceOrientation, Never>) {
        self.interfaceOrientationSubject = interfaceOrientationSubject
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        // Set initial interfaceOrientation
        interfaceOrientationSubject.send(
            view.window?.windowScene?.interfaceOrientation ?? .unknown
        )
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

        // Update interfaceOrientation
        interfaceOrientationSubject.send(
            view.window?.windowScene?.interfaceOrientation ?? .unknown
        )
    }
}
				
			

2. Wrap view controller into SwiftUI view:

				
					struct InterfaceOrientationProviderView: UIViewControllerRepresentable {
    let interfaceOrientationSubject: CurrentValueSubject<UIInterfaceOrientation, Never>
    
    func makeUIViewController(context: Context) -> InterfaceOrientationProviderVC {
        InterfaceOrientationProviderVC(interfaceOrientationSubject: interfaceOrientationSubject)
    }
    
    func updateUIViewController(_ uiViewController: InterfaceOrientationProviderVC, context: Context) { }
}
				
			

3. Create ViewModifier which will observe interfaceOrientation changes and pass them to SwiftUI via environment value:

				
					private struct InterfaceOrientationKey: EnvironmentKey {
    static let defaultValue: InterfaceOrientation = .portrait
}

extension EnvironmentValues {
    var interfaceOrientation: InterfaceOrientation {
        get { self[InterfaceOrientationKey.self] }
        set { self[InterfaceOrientationKey.self] = newValue }
    }
}

private struct InterfaceOrientationViewModifier: ViewModifier {
    private let interfaceOrientationSubject: CurrentValueSubject<UIInterfaceOrientation, Never> = .init(.portrait)
    @State private var interfaceOrientation: InterfaceOrientation = .portrait

    func body(content: Content) -> some View {
        ZStack {
            InterfaceOrientationProviderView(interfaceOrientationSubject: interfaceOrientationSubject)
            
            content
                .onReceive(interfaceOrientationSubject, perform: { orientation in
                    self.interfaceOrientation = orientation.swiftUI
                })
                .environment(\.interfaceOrientation, interfaceOrientation)
        }
    }
}

extension UIInterfaceOrientation {
    /// Map from UIKit's UIInterfaceOrientation to SwiftUI's InterfaceOrientation
    var swiftUI: InterfaceOrientation {
        switch self {
        case .portrait: return .portrait
        case .landscapeLeft: return .landscapeLeft
        case .landscapeRight: return .landscapeRight
        case .portraitUpsideDown: return .portraitUpsideDown
            
        case .unknown: return .portrait
        @unknown default: return .portrait
        }
    }
}
				
			

4. Add convenient method to apply our view modifier:

				
					extension View {
    func observeIntefaceOrientation() -> some View {
        self.modifier(InterfaceOrientationViewModifier())
    }
}
				
			

5. Apply this modifier to main application/window view

				
					@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .observeIntefaceOrientation()
        }
    }
}
				
			

And after that we can use it in our views:

				
					struct SomeView: View {
    @Environment(\.interfaceOrientation) private var interfaceOrientation

    var body: some View {
        HStack {
            Text("Current interface orientation:")
            Text(interfaceOrientation.id)
        }
    }
}