Detecting Device Landscape Orientation in SwiftUI


Greetings, traveler!

When working with adaptive layouts in SwiftUI, one common requirement is to determine whether the device is currently in portrait or landscape mode. While UIKit provides UIDevice.current.orientation, it often produces unreliable results — especially when the device is lying flat. In such cases, values like .faceUp or .faceDown are returned, and UIDeviceOrientation.isLandscape may not reflect the actual screen layout.

A more robust approach in SwiftUI is to analyze the screen’s aspect ratio using GeometryReader. This technique focuses on how content is rendered on screen rather than relying on sensor-based orientation, providing a reliable basis for responsive layout adjustments.

The method involves comparing the width and height of the available geometry space. If width > height, the screen is considered to be in landscape layout. This offers a clear and deterministic way to drive layout logic, regardless of the physical orientation reported by the device.

Below is a simple SwiftUI ViewModifier that exposes this logic through a binding, making it reusable across views.

import SwiftUI

struct DeviceOrientationModifier: ViewModifier {
    @Binding var isLandscape: Bool

    func body(content: Content) -> some View {
        GeometryReader { geometry in
            content
                .onAppear {
                    isLandscape = geometry.size.width > geometry.size.height
                }
                .onChange(of: geometry.size) { newSize in
                    isLandscape = newSize.width > newSize.height
                }
        }
    }
}

extension View {
    func detectDeviceOrientation(isLandscape: Binding<Bool>) -> some View {
        self.modifier(DeviceOrientationModifier(isLandscape: isLandscape))
    }
}

To apply this modifier, simply add it to your root view and provide a state variable to observe orientation changes:

struct ContentView: View {
    @State private var isLandscape = false

    var body: some View {
        VStack {
            Text("Orientation: \(isLandscape ? "Landscape" : "Portrait")")
        }
        .detectDeviceOrientation(isLandscape: $isLandscape)
    }
}

This approach is particularly useful when you need to reorganize UI layouts based on how the screen is being utilized, such as switching between horizontal and vertical stacks, altering grid columns, or toggling visibility of certain interface elements.

By relying on the aspect ratio of the view’s frame rather than the device’s physical orientation, you ensure that your UI reacts appropriately in all scenarios — including when the device is flat on a surface or embedded in a fixed position, such as a stand or dock.

Handling Orientation Ignoring Safe Area

One important caveat of using GeometryReader or scene-level size monitoring is that the reported view size may change when the keyboard appears. In such cases, the system reduces the available height of the view hierarchy, which can cause your orientation detection logic to temporarily think the device has switched to landscape mode.

To avoid this, you can base your orientation detection on the actual size, which is not affected by the keyboard. The following modifier exposes a stable isLandscape value and ignores keyboard-driven layout changes:

struct DeviceOrientationModifier: ViewModifier {
    @Binding var isLandscape: Bool
    
    func body(content: Content) -> some View {
        ZStack {
            GeometryReader { geometry in
                Color.clear
                    .onAppear {
                        isLandscape = size.width > size.height
                    }
                    .onChange(of: geometry.size) { _, newSize in
                        isLandscape = size.width > size.height
                    }
            }
            .ignoresSafeArea(.keyboard)
            
            content
        }
    }
}

However, in addition to keyboard, we also have safeAreaBar, which can also affect the result. Therefore, you may wat to apply not only .keyboard but also use this modifier without specifying parameters, like this:

struct DeviceOrientationModifier: ViewModifier {
    @Binding var isLandscape: Bool
    
    func body(content: Content) -> some View {
        ZStack {
            GeometryReader { geometry in
                Color.clear
                    .onAppear {
                        isLandscape = size.width > size.height
                    }
                    .onChange(of: geometry.size) { _, newSize in
                        isLandscape = size.width > size.height
                    }
            }
            .ignoresSafeArea()
            
            content
        }
    }
}

Extended version

Fabio Floris expanded on this idea and created a component that allows using @Environment to read the orientation from the root view. Here is the link to his article.

Using horizontalSizeClass

Another tool worth mentioning is the horizontalSizeClass provided by the SwiftUI environment. Although it is often associated with layout changes that coincide with device rotation, it is important to clarify that a size class does not represent orientation. Instead, it describes the type of interface space currently available — compact or regular.

This distinction matters because different devices and multitasking configurations can produce different size-class results for the same physical orientation. For example, an iPad in Split View may report a compact horizontal size class even in landscape, while an iPhone in landscape might still remain compact. For that reason, size classes should be viewed as a high-level tool for adapting UI to “narrow” or “wide” environments, not as a reliable mechanism for detecting the device’s orientation. That said, they are often a recommended option when your goal is simply to adjust layout based on available space rather than to determine the exact orientation.

Using ViewThatFits

For cases where your goal is not to detect orientation but simply to switch between different layouts depending on the available space, SwiftUI’s ViewThatFits can offer an even cleaner solution. Introduced in iOS 16, this container evaluates its children in order and displays the first one that fits into the current constraints.

In practice, this allows you to provide, for example, a horizontal layout followed by a vertical one, and let SwiftUI automatically choose the most suitable option. This approach removes the need for manual size checks and often results in simpler, more resilient code. If your use case is purely about adapting layout rather than reacting to orientation, ViewThatFits may be the most appropriate and future-proof tool.

Conclusion

Detecting orientation in SwiftUI remains a nuanced task. While relying on raw device orientation is rarely appropriate, inspecting the available geometry provides a practical and predictable way to understand how much horizontal or vertical space the interface currently has. Size classes can complement this approach when you need to adapt the UI based on broader interface categories, though they should not be mistaken for a direct orientation signal. And in many cases, explicit orientation checks are not required at all — ViewThatFits allows the layout to adapt naturally to the constraints of the container.