M. Koot
M. Koot

Reputation: 1176

How to access safe area size in SwiftUI?

I want to manually set the frame height of a view in SwiftUI to the size of the safe area of the screen. It's easy to get the bounds of the screen (UIScreen.main.bounds), but I can't find a way to access the size of the safe area.

Upvotes: 81

Views: 72561

Answers (7)

Codelaby
Codelaby

Reputation: 2873

I adapt for XCode15 IOS17

extension UIApplication {
    static var keyWindow: UIWindow? {
        UIApplication.shared
            .connectedScenes.lazy
            .compactMap { $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil }
            .first(where: { $0.keyWindow != nil })?
            .keyWindow
    }
}

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
        UIApplication.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
    }
}

extension EnvironmentValues {
    var safeAreaInsets: EdgeInsets {
        self[SafeAreaInsetsKey.self]
    }
}

private extension UIEdgeInsets {
    var swiftUiInsets: EdgeInsets {
        EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
    }
}

Usage


struct ContentView: View {

    @Environment(\.safeAreaInsets) private var safeAreaInsets

    var body: some View {
        ZStack {
            Color.blue
            Text("Hello world").foregroundColor(.white)
            .padding(safeAreaInsets)
        }
    }
}

Extract value .top safeArea safeAreaInsets.top

Debug values

let _ = print("safe area: \(safeAreaInsets)"

You can rotate device and see all values

Upvotes: 1

Bobby
Bobby

Reputation: 6255

extension UIScreen {
    static var topSafeArea: CGFloat {
        let keyWindow = UIApplication.shared.connectedScenes
            .filter({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene})
            .compactMap({$0})
            .first?.windows
            .filter({$0.isKeyWindow}).first
        
        return (keyWindow?.safeAreaInsets.top) ?? 0
    } 
}

Usage:

UIScreen.topSafeArea

Upvotes: 4

Kai Zheng
Kai Zheng

Reputation: 8130

You can also create a custom EnvironmentValue and pass the safe area insets over from an "initial View". This works perfectly for me!

Creating EnvironmentValue

private struct SafeAreaInsetsEnvironmentKey: EnvironmentKey {
    static let defaultValue: (top: CGFloat, bottom: CGFloat) = (0, 0)
}

extension EnvironmentValues {
    var safeAreaInsets: (top: CGFloat, bottom: CGFloat) {
        get { self[SafeAreaInsetsEnvironmentKey.self] }
        set { self[SafeAreaInsetsEnvironmentKey.self] = newValue }
    }
}

Setting

The idea is to do this before any potential View parent uses .edgesIgnoringSafeArea, this is required for it to work. For instance:

@main
struct YourApp: App {
    @State private var safeAreaInsets: (top: CGFloat, bottom: CGFloat) = (0, 0)
    
    var body: some Scene {
        WindowGroup {
            ZStack {
                GeometryReader { proxy in
                    Color.clear.onAppear {
                        safeAreaInsets = (proxy.safeAreaInsets.top, proxy.safeAreaInsets.bottom)
                    }
                }
                
                ContentView()
                    .environment(\.safeAreaInsets, safeAreaInsets)
            }
        }
    }
}

Usage

struct SomeChildView: View {
    @Environment(\.safeAreaInsets) var safeAreaInsets

    ...
}

Upvotes: 9

Phil Dukhov
Phil Dukhov

Reputation: 87804

Not sure why the accepted answer uses top inset for a view placed under the bottom one - these are not the same. Also if you correct this "typo", you'll see that edgesIgnoringSafeArea called on a GeometryReader zeros the corresponding value. Looks like it wasn't the case back on iOS 13, but now it is, so you need to call edgesIgnoringSafeArea on a GeometryReader child instead, and this code still works as expected on iOS 13:

GeometryReader { geometry in
    VStack {
        Spacer()
        Color.red
            .frame(
                width: geometry.size.width,
                height: geometry.safeAreaInsets.bottom,
                alignment: .center
            )
            .aspectRatio(contentMode: ContentMode.fit)
    }
    .edgesIgnoringSafeArea(.bottom)
}

Upvotes: 16

Mirko
Mirko

Reputation: 2466

UIApplication.shared.windows is deprecated, you can now use connectedScenes:

import SwiftUI

extension UIApplication {
    var keyWindow: UIWindow? {
        connectedScenes
            .compactMap {
                $0 as? UIWindowScene
            }
            .flatMap {
                $0.windows
            }
            .first {
                $0.isKeyWindow
            }
    }
}

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
        UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
    }
}

extension EnvironmentValues {
    var safeAreaInsets: EdgeInsets {
        self[SafeAreaInsetsKey.self]
    }
}

private extension UIEdgeInsets {
    var swiftUiInsets: EdgeInsets {
        EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
    }
}

And then use Environment property in your View to get safe area insets:

@Environment(\.safeAreaInsets) private var safeAreaInsets

Upvotes: 50

Lorenzo Fiamingo
Lorenzo Fiamingo

Reputation: 4069

If you use edgesIgnoringSafeArea on an parentView and you want to access the device UISafeAreaInsets you can do the following:

Code

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
        (UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero).insets
    }
}

extension EnvironmentValues {
    
    var safeAreaInsets: EdgeInsets {
        self[SafeAreaInsetsKey.self]
    }
}

private extension UIEdgeInsets {
    
    var insets: EdgeInsets {
        EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
    }
}

Usage

struct MyView: View {
    
    @Environment(\.safeAreaInsets) private var safeAreaInsets
    
    var body: some View {
        Text("Ciao")
            .padding(safeAreaInsets)
    }
}

Upvotes: 52

Ugo Arangino
Ugo Arangino

Reputation: 2958

You can use a GeometryReader to access the safe area.
See: https://developer.apple.com/documentation/swiftui/geometryreader.

struct ContentView : View {
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Spacer()
                Color.red
                    .frame(
                        width: geometry.size.width,
                        height: geometry.safeAreaInsets.top,
                        alignment: .center
                )
                    .aspectRatio(contentMode: ContentMode.fit)
            }
        }
        .edgesIgnoringSafeArea(.bottom)
    }
}

But FYI: The safe area is not a size. It is an EdgeInsets.

Upvotes: 99

Related Questions