0

I have 2 views:

import SwiftUI

struct SwiftUIView: View {
    var body: some View {
        NavigationStack {
            ... //show SwiftUIView2
        }
        .toolbar {
            ...
        }
        .overlay {
            ... //fullscreen overlay
        }
    }
}

#Preview {
    SwiftUIView()
}
import SwiftUI

struct SwiftUIView2: View {
    var body: some View {
        NavigationStack {
            ...
        }
        .toolbar {
            ...
        }
        .overlay {
            ... //fullscreen overlay
        }
    }
}

#Preview {
    SwiftUIView()
}

The first overlay works as expected. The second overlay overlays everything except toolbar buttons. How to fix this issue?

Tried different ways but all of them unsuccessful. Tried .fullScreenCover(...) but it has its own problems with animation, sequential dismissing-presenting and etc.

Example

struct OverlayView: ViewModifier {
    @Binding var isPresented: Bool
    let modalContent: AnyView

    func body(content: Content) -> some View {
        ZStack {
            content
            if isPresented {
                modalContent
                    .edgesIgnoringSafeArea(.all)
            }
        }
    }
}

extension View {
    func overlayModal(isPresented: Binding<Bool>, @ViewBuilder modalContent: () -> some View) -> some View {
        self.modifier(OverlayView(isPresented: isPresented, modalContent: AnyView(modalContent())))
    }
}

struct ContentView: View {
    @State var showModal = false
    @State var showNext = false
    
    var body: some View {
        NavigationStack {
            Button {
                showModal = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    showModal = false
                    showNext = true
                }
            } label: {
                VStack {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundStyle(.tint)
                    Text("Hello, world!")
                }
                .padding()
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    NavigationLink {
                        EmptyView()
                    } label: {
                        Rectangle()
                            .frame(width: 44, height: 44)
                    }
                }
            }
            .navigationDestination(isPresented: $showNext) {
                ContentView()
            }
        }
        .overlayModal(isPresented: $showModal) {
            Color.black.opacity(0.5)
        }
    }
}
6
  • You shouldn't need another NavigationStack in SwiftUIView2, because this view is inside the first NavigationStack. Anyway, a workaround for the overlay not covering the toolbar buttons might be to hide the toolbar when the overlay is showing. For more detail, please elaborate your example so that it actually shows the toolbar and overlays. Commented Mar 5 at 19:58
  • @BenzyNeez added example. Slightly simplified - the view "pushes" another instance of itself. If I remove NavigationStack from the second instance nothing changes Commented Mar 6 at 11:41
  • OK thanks for the update, answer posted. By navigating to another instance of ContentView you still have a nested NavigationStack. I would suggest, trying to avoid this in your real code. Commented Mar 6 at 12:13
  • @Gargo Very curious as to why you're pushing to another instance of the same view? Why not just close the modal and remain on the same view? Commented Mar 6 at 18:18
  • @Gargo Also, your first two code blocks with SwiftUIView and SwiftUIView2 seem to be irrelevant and somewhat misleading, since the "second" view is actually the same view and not a different one. I suggest removing them and just keeping the actual example. Commented Mar 6 at 18:39

3 Answers 3

1

One way to resolve the problem is to hide the navigation bar when the overlay is showing. This can be done using toolbarVisibility(_:for:) (iOS 18) or toolbar(_:for:) (earlier versions).

The only problem with this technique is that the content of the page moves up into the space that was previously occupied by the toolbar. If your overlay is semi-transparent (as in your example), you will see this movement happening below the overlay.

A workaround for the movement is to add padding to compensate for the change in height. The change in height is actually a change to the top safe area inset. This can be measured by wrapping the content with a GeometryReader:

@State private var topPadding = CGFloat.zero
NavigationStack {
    GeometryReader { proxy in
        Button {
            // ...
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .safeAreaPadding(.top, topPadding)
        .onChange(of: proxy.safeAreaInsets.top) { oldVal, newVal in
            topPadding = showModal ? max(0, oldVal - newVal) : 0
        }
        .toolbar {
            // ...
        }
        . toolbarVisibility(showModal ? .hidden : .visible, for: .navigationBar)
        // Pre iOS 18:
        // .toolbar(showModal ? .hidden : .visible, for: .navigationBar)
        .navigationDestination(isPresented: $showNext) {
            ContentView()
        }
    }
}
.overlayModal(isPresented: $showModal) {
    Color.black.opacity(0.5)
}

I can't help thinking, that a .fullScreenCover might be a simpler solution. You mentioned that this caused issues with animation and sequential presentation. See Custom fullScreenCover animation for a way to animate the cover. If you could elaborate on what exactly the other issues are, then perhaps workarounds can be found for them too. This might be best addressed as a new question.

Sign up to request clarification or add additional context in comments.

5 Comments

1)works in iOS 18 only. So it will work for 7.2% of users only; 2)I saw somewhere a way to hide it but there is another problem - it influences on the content size
it hides toolbar but also "stretches" content because of more free space. It is not enough.
@Gargo Answer updated again. Re. iOS 18, Apple claims that 68% of all devices are using iOS 18, see App Store
did the similar thing via the simpler way - by using an UIViewRepresentable overlay which accesses navigation bar an temporarily hides it by setting layer.zPosition=-1. It hides the navigation bar and preserves frames but may break transition animation
@Gargo I posted an answer that may be simpler than all that kung-fu of using an UIViewRepresentable.
1

Although I am not sure why you'd set things up this way, a solution would be to simply hide the toolbar items when the modal is showing (not the toolbar itself which would cause the layout shift).

To hide the trailing toolbar item, you can simply use opacity:

.toolbar {
    ToolbarItem(placement: .topBarTrailing) {
        NavigationLink {
            EmptyView()
        } label: {
            Rectangle()
                .frame(width: 44, height: 44)
        }
        .opacity(showModal ? 0 : 1) // <- Here, hide toolbar button when modal is showing
    }
}

And to hide the navigation back button:

.navigationBarBackButtonHidden(showModal ? true : false) // <- Here, hide back button when modal is showing

Here's the complete code:

import SwiftUI

//Root view
struct SwiftUIOverlayContentView: View {
    
    @State var showModal = false
    @State var showNext = false
    
    var body: some View {
        NavigationStack {
            Button {
                showModal = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    showModal = false
                    showNext = true
                }
            } label: {
                VStack {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundStyle(.tint)
                    Text("Hello, world!")
                }
                .padding()
            }
            .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        NavigationLink {
                            EmptyView()
                        } label: {
                            Rectangle()
                                .frame(width: 44, height: 44)
                        }
                        .opacity(showModal ? 0 : 1) // <- Here, hide toolbar button when modal is showing
                    }
            }
            .navigationBarBackButtonHidden(showModal ? true : false) // <- Here, hide back button when modal is showing
            .navigationDestination(isPresented: $showNext) {
                SwiftUIOverlayContentView()
            }
        }
        .overlayModal(isPresented: $showModal) {
            Color.black.opacity(0.5)
        }
    }
}

//Overlay View Modifier
struct OverlayView: ViewModifier {
    @Binding var isPresented: Bool
    let modalContent: AnyView
    
    func body(content: Content) -> some View {
        ZStack {
            content
            if isPresented {
                modalContent
                    .edgesIgnoringSafeArea(.all)
            }
        }
    }
}

//View extension
extension View {
    func overlayModal(isPresented: Binding<Bool>, @ViewBuilder modalContent: () -> some View) -> some View {
        self.modifier(OverlayView(isPresented: isPresented, modalContent: AnyView(modalContent())))
    }
}

//Preview
#Preview {
    SwiftUIOverlayContentView()
}

Comments

0

My current solution:

import SwiftUI

// Shared State for Overlay Visibility
class OverlayManager: ObservableObject {
    static let shared = OverlayManager()
    
    @AppStorage("showOverlayOnRoot") var showOverlayOnRoot = false
    @AppStorage("showOverlay2OnRoot") var showOverlay2OnRoot = false
    @AppStorage("showOverlay3OnRoot") var showOverlay3OnRoot = false
}

struct SwiftUIView: View {
    @StateObject private var overlayManager = OverlayManager.shared
    @State private var showNextView = false

    var body: some View {
        NavigationStack {
            VStack {
                Text("SwiftUIView")
                NavigationLink("Go to SwiftUIView2") {
                    SwiftUIView2()
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    NavigationLink {
                        EmptyView()
                    } label: {
                        Rectangle()
                            .frame(width: 44, height: 44)
                    }
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Show Overlay") {
                        overlayManager.showOverlayOnRoot = true
                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                            overlayManager.showOverlayOnRoot = false
                        }
                    }
                }
            }
        }
        .overlayModal(isPresented: $overlayManager.showOverlayOnRoot) {
            Color.black.opacity(0.5)
                .overlay(
                    Text("Overlay on Root")
                        .foregroundColor(.white)
                )
        }
        .overlayModal(isPresented: $overlayManager.showOverlay2OnRoot) {
            Color.red.opacity(0.5)
                .overlay(
                    Text("Overlay 2 on Root")
                        .foregroundColor(.white)
                )
        }
        .overlayModal(isPresented: $overlayManager.showOverlay3OnRoot) {
            Color.blue.opacity(0.5)
                .overlay(
                    Text("Overlay 3 on Root")
                        .foregroundColor(.white)
                )
        }
    }
}

struct SwiftUIView2: View {
    var body: some View {
        NavigationStack {
            VStack {
                Text("SwiftUIView2")
                Button("Show Overlay 2 on Root") {
                    OverlayManager.shared.showOverlay2OnRoot = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        OverlayManager.shared.showOverlay2OnRoot = false
                    }
                }
                NavigationLink("Go to SwiftUIView3") {
                    SwiftUIView3()
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    NavigationLink {
                        EmptyView()
                    } label: {
                        Rectangle()
                            .frame(width: 44, height: 44)
                    }
                }
            }
        }
    }
}

struct SwiftUIView3: View {
    var body: some View {
        VStack {
            Text("SwiftUIView3")
            Button("Show Overlay 3 on Root") {
                OverlayManager.shared.showOverlay3OnRoot = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                    OverlayManager.shared.showOverlay3OnRoot = false
                }
            }
        }
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                NavigationLink {
                    EmptyView()
                } label: {
                        Rectangle()
                            .frame(width: 44, height: 44)
                    }
            }
        }
    }
}

#Preview {
    SwiftUIView()
}

The idea - I place each fullscreen overlay into the root view (where it still can overlay toolbar). It is not convenient but at least works from any part of the app, allows different overlays and there is no need to hide-show navigation bar. Maybe someone could suggest better.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.