iosswiftuiuikituiwindow

SwiftUI detect top notch and safe area insets


I am looking for a pure SwiftUI based solution to determine whether the iOS device has top notch based on the safe area insets of the root view. While this is easy to determine in UIKit, for SwiftUI the only solution that I have found so far is also UIKittish, that is:

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

 private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
      UIApplication.shared.currentWindow?.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)
   }
 }

While it's likely that SwiftUI will continue to use UIKit elements such as UIWindow under the hood so the solution above will continue to work for next several years, I still want to know if there is a pure SwiftUI based solution to this.


Solution

  • You can get safe area insets from a GeometryReader. You can put a GeometryReader at the very top of the view hierarchy, perhaps directly in WindowGroup { ... }. Then you can send the insets down with an environment key.

    WindowGroup {
        GeometryReader { geo in
            // putting the text directly in a GeometryReader makes it align to the top left
            // which is why I am nesting it in a ZStack that covers the whole screen here
            // any other view that fills all the available space works too
            ZStack {
                ContentView() // the rest of the app is here 
                    .environment(\.safeAreaInsets, geo.safeAreaInsets)
            }.frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
    
    struct ContentView: View {
        @Environment(\.safeAreaInsets) var insets
        var body: some View {
            Text("\(insets.top)")
        }
    }
    
    private struct SafeAreaInsetsKey: EnvironmentKey {
        static var defaultValue: EdgeInsets = .init()
    }
    
    extension EnvironmentValues {
        var safeAreaInsets: EdgeInsets {
            get { self[SafeAreaInsetsKey.self] }
            set { self[SafeAreaInsetsKey.self] = newValue }
        }
    }
    

    Note that you need to preview the whole GeometryReader for this to work in Xcode Previews. You can encapsulate the geometry reader and ZStack into a view modifier like this, so that you can conveniently use it in Previews:

    extension View {
        func readSafeAreaInsets() -> some View {
            GeometryReader { geo in
                ZStack {
                    self.environment(\.safeAreaInsets, geo.safeAreaInsets)
                }.frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    }
    
    #Preview {
        ContentView().readSafeAreaInsets()
    }