swiftuimodal-dialogoverlayfullscreenswiftui-navigationstack

show fullscreen view with overlay in SwiftUI?


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)
        }
    }
}

Solution

  • 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.