“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
init(interfaceOrientationSubject: CurrentValueSubject) {
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
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 = .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)
}
}
}