How to Read Safe Area Insets in SwiftUI

How to Read Safe Area Insets in SwiftUI

Written by

in

Quick answer: In SwiftUI, wrap your view in a GeometryReader and inspect geometry.safeAreaInsets.top and geometry.safeAreaInsets.bottom. In UIKit, avoid deprecated keyWindow calls and read view.safeAreaInsets inside viewDidLayoutSubviews(), or query the active UIWindowScene’s key window.

Why this happens

Accessing top and bottom safe area insets frequently yields unexpected values—often returning 0.0—because safe area metrics do not exist in isolation. They are calculated dynamically by UIKit and SwiftUI only after a view connects to an active window hierarchy and the layout engine computes the frame relative to hardware cutouts, status bars, and home indicators.

Reading view.safeAreaInsets in viewDidLoad() or init fails because the view controller has not yet been placed into the window hierarchy. At that initial lifecycle stage, the system has not calculated the surrounding margins, so all edge insets default to zero.

Additionally, legacy code often relied on UIApplication.shared.keyWindow or UIApplication.shared.windows. Apple deprecated these properties with the introduction of multiple window scenes in iOS 13. Accessing windows across scenes without targeting the active UIWindowScene produces compiler warnings, crashes in multi-window environments, or incorrect insets on devices with dynamic notches or split displays.

How to fix it

1. Read insets in SwiftUI using GeometryReader

In SwiftUI, the canonical way to inspect hardware insets without breaking layout hierarchy is through GeometryProxy.

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            VStack(spacing: 12) {
                Text("Top Inset: \(geometry.safeAreaInsets.top, specifier: "%.1f") pt")
                Text("Bottom Inset: \(geometry.safeAreaInsets.bottom, specifier: "%.1f") pt")
                Text("Leading Inset: \(geometry.safeAreaInsets.leading, specifier: "%.1f") pt")
                Text("Trailing Inset: \(geometry.safeAreaInsets.trailing, specifier: "%.1f") pt")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

If you need the outer window insets inside a nested view that has already had its safe area modified by parent views, read the geometry inside a background layer before ignoring safe areas:

import SwiftUI

struct FullWindowInsetsKey: PreferenceKey {
    static var defaultValue: EdgeInsets = EdgeInsets()
    static func reduce(value: inout EdgeInsets, nextValue: () -> EdgeInsets) {
        value = nextValue()
    }
}

struct WindowInsetReader: View {
    @State private var windowInsets = EdgeInsets()

    var body: some View {
        VStack {
            Text("Top Window Padding: \(windowInsets.top, specifier: "%.1f")")
            Text("Bottom Window Padding: \(windowInsets.bottom, specifier: "%.1f")")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(
            GeometryReader { proxy in
                Color.clear
                    .preference(key: FullWindowInsetsKey.self, value: proxy.safeAreaInsets)
            }
            .ignoresSafeArea()
        )
        .onPreferenceChange(FullWindowInsetsKey.self) { insets in
            self.windowInsets = insets
        }
    }
}

2. Read insets in UIViewController lifecycle methods

In UIKit, safeAreaInsets are only valid once the view has been added to a window. Read or respond to changes inside viewSafeAreaInsetsDidChange() or viewDidLayoutSubviews(), never in viewDidLoad().

import UIKit

final class DetailViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        // Do not read view.safeAreaInsets here; it returns .zero
    }

    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()

        let topPadding = view.safeAreaInsets.top
        let bottomPadding = view.safeAreaInsets.bottom
        print("Safe area updated - Top: \(topPadding), Bottom: \(bottomPadding)")
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let safeHeight = view.safeAreaLayoutGuide.layoutFrame.height
        let totalHeight = view.bounds.height
        print("Usable safe height: \(safeHeight) of \(totalHeight)")
    }
}

3. Query the active UIWindowScene key window (Modern UIKit)

If you must retrieve safe area insets outside of a view controller hierarchy (such as in an app coordinator or utility class), resolve the active foreground UIWindowScene instead of calling deprecated UIApplication properties.

import UIKit

extension UIWindow {
    static var activeKeyWindow: UIWindow? {
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .filter { $0.activationState == .foregroundActive }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }
    }
}

struct DevicePadding {
    static var safeArea: UIEdgeInsets {
        UIWindow.activeKeyWindow?.safeAreaInsets ?? .zero
    }

    static var top: CGFloat {
        safeArea.top
    }

    static var bottom: CGFloat {
        safeArea.bottom
    }
}

This approach eliminates the Xcode deprecation warning 'keyWindow' was deprecated in iOS 13.0 and ensures compatibility with iPadOS split view and stage manager configurations.

4. Derive dimensions from safeAreaLayoutGuide

When calculating content offsets, manual drawing rects, or custom collection view layouts, compute offsets directly through UILayoutGuide rather than hardcoding inset subtraction.

import UIKit

func calculateUsableHeight(for viewController: UIViewController) -> CGFloat {
    let guide = viewController.view.safeAreaLayoutGuide
    return guide.layoutFrame.size.height
}

func calculateGuideOffsets(for view: UIView) -> (top: CGFloat, bottom: CGFloat) {
    let frame = view.bounds
    let safeFrame = view.safeAreaLayoutGuide.layoutFrame

    let topOffset = safeFrame.minY - frame.minY
    let bottomOffset = frame.maxY - safeFrame.maxY

    return (topOffset, bottomOffset)
}

How to prevent it

  • Do not access view.safeAreaInsets inside viewDidLoad() or view initialization methods.
  • Avoid hardcoding notch heights (e.g., 44 or 47 points) or bottom home indicator dimensions (e.g., 34 points); hardware insets vary across device generations and display zoom settings.
  • Use Auto Layout constraints pinned to view.safeAreaLayoutGuide instead of manually setting frames via inset calculations.
  • In SwiftUI, leverage native container padding or the .safeAreaInset(edge:alignment:spacing:content:) modifier rather than manually adding spacer heights.
  • Migrate away from UIApplication.shared.keyWindow by inspecting window collections on active UIWindowScene instances.

FAQ

Why is view.safeAreaInsets returning zero in my UIViewController?

view.safeAreaInsets returns UIEdgeInsets.zero until the view is attached to a window and performs its first layout pass. Move your calculation to viewDidLayoutSubviews() or override viewSafeAreaInsetsDidChange().

How do I add custom floating views above the safe area in SwiftUI?

Use the .safeAreaInset(edge: .bottom) view modifier available in iOS 15+. This modifier automatically repositions scrollable content so that your persistent toolbar or floating view does not obscure interactive content.

Does safe area inset include the navigation bar and tab bar?

Yes. A view controller’s safeAreaInsets accounts for system hardware cutouts as well as translucent or opaque system bars managed by navigation controllers and tab bar controllers enclosing that view.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.