'Additional safe area on NavigationView in SwiftUI
I am building a SwiftUI app where I have an overlay that is conditionally shown across my entire application like this:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.safeAreaInset(edge: .bottom) {
Group {
if myCondition {
EmptyView()
} else {
OverlayView()
}
}
}
}
}
}
I would expect this to adjust the safe area insets of the NavigationView and propagate it to any content view, so content is not stuck under the overlay. At least that's how additionalSafeAreaInsets in UIKit would behave. Unfortunately, it seems that SwiftUI ignores any safeAreaInsets() on a NavigationView (the overlay will show up, but safe area is not adjusted).
While I can use a GeometryReader to read the overlay size and then set safeAreaInsets() on ContentView, this will only work for ContentView - as soon as I navigate to the next view the safe area is gone.
Is there any nice way to get NavigationView to accept additional safe area insets, either by using safeAreaInsets() or by some other way?
Solution 1:[1]
So it seems NavigationView does not adjust its safe area inset when using .safeAreaInset. If this is intended or a bug is not clear to me. Anyway, I solved this for now like this (I wanted to use pure SwiftUI, using UIKit's additionalSafeAreaInsets might be an option to):
Main App File:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.environmentObject(SafeAreaController.shared)
.safeAreaInset(edge: .bottom) {
OverlayView()
.frameReader(safeAreaController.updateAdditionalSafeArea)
}
}
}
}
class SafeAreaController: ObservableObject {
static let shared = SafeAreaController()
@Published private(set) var additionalSafeArea: CGRect = .zero
func updateAdditionalSafeArea(_ newValue: CGRect) {
if newValue != additionalSafeArea {
additionalSafeArea = newValue
}
}
}
struct FrameReader: ViewModifier {
let changeChandler: ((CGRect) -> Void)
init(_ changeChandler: @escaping (CGRect) -> Void) {
self.changeChandler = changeChandler
}
func body(content: Content) -> some View {
content
.background(
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
let newFrame = geometry.frame(in: .global)
changeChandler(newFrame)
}
return Color.clear
}
)
}
}
extension View {
func frameReader(_ changeHandler: @escaping (CGRect) -> Void) -> some View {
return modifier(FrameReader(changeHandler))
}
}
EVERY Content View that is pushed on your NavigationView:
struct ContentView: View {
@EnvironmentObject var safeAreaController: SafeAreaController
var body: some View {
YourContent()
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: safeAreaController.additionalSafeArea.height)
}
}
Why does it work?
- In the main app file, a
GeometryReaderis used to read the size of the overlay created insidesafeAreaInset(). The size is written to the sharedSafeAreaController - The shared
SafeAreaControlleris handed as anEnvironmentObjectto every content view of our navigation - An invisible object is created as the
.safeAreaInsetof every content view with the height read from theSafeAreaController- this will basically create an invisible bottom safe area that is the same size as our overlay, thus making room for the overlay
Solution 2:[2]
struct ContentView: View {
var body: some View {
NavigationView {
ListView()
.navigationBarTitle("Test")
}
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
Text("My Overlay")
.padding()
Spacer()
}
.background(.ultraThinMaterial)
}
}
}
struct ListView: View {
var body: some View {
List(0..<30) { item in
NavigationLink {
Text("Next view")
} label: {
Text("Item \(item)")
}
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 |
