Reza Khonsari
Reza Khonsari

Reputation: 466

How to manage multiple sheets within a NavigationStack in SwiftUI

I am working with sheets in SwiftUI and recently found a problem with navigation while a sheet is already open.
In the code below i have a navigation stack that navigates to two different detail view:

struct ContentView: View {

    @State var navigationPath = NavigationPath()

    var body: some View {
        NavigationStack(path: $navigationPath) {
            VStack {
                Text("Go to first detail")
                Button("Submit") {
                    navigationPath.append("FirstDetail")
                }
            }
            .padding()
            .navigationDestination(for: String.self) { value in
                if value == "FirstDetail" {
                    FirstDetailView(path: $navigationPath)
                }

                if value == "SecondDetail" {
                    SecondDetailView()
                }
            }
        }
    }
}

In the code below is my first detail view that already present sheet and also has a button to navigate to the second detail view:

struct FirstDetailView: View {
    @Binding var path: NavigationPath
    @State var isPresented = true

    var body: some View {
        VStack {
            Text("Hello First!")
            Button("GoToSecond", action: {
                path.append("SecondDetail")
            })
            Spacer()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.yellow)
        }
        .sheet(isPresented: $isPresented) {
            SheetDetailView(title: "First")
                .presentationDetents([.medium, .large])
                .presentationBackgroundInteraction(.enabled)
        }
    }
}

also in my second detail view, I have another sheet:

struct SecondDetailView: View {
    
    @State var isPresented = true
    
    var body: some View {
        VStack {
            Text("Hello Second")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.green)
        }
        .sheet(isPresented: $isPresented) {
            SheetDetailView(title: "Second")
                .presentationDetents([.medium, .large])
                .interactiveDismissDisabled()
                .presentationBackgroundInteraction(.enabled)
        }
    }
}

And here is my sheet detail view:

struct SheetDetailView: View {
    var title: String

    var body: some View {
        Text("Hello \(title)")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.red)
    }
}

The result will be like this:

Demo

So my issue is that navigation to another view while the first sheet is open causes this sheet to appear in the second detail view so each of detailview first and second has its own sheet, and also I want to know what is the best approach for this issue.

Upvotes: 1

Views: 608

Answers (2)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119917

It depends on the desired output, a simple way would delay the presentation:

  1. Make the default isPresented value equal to false
  2. Make it true with a delay

So you will have:

Demo

Changed code:

struct FirstDetailView: View {
    @Binding var path: NavigationPath
    @State var isPresented = false // 👈 Default this to false

    var body: some View {
        VStack {
            Text("Hello First!")
            Button("GoToSecond", action: {
                path.append("SecondDetail")
                isPresented = false // 👈 You can ignore this to have different appearance
            })
            Spacer()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.yellow)
        }
        .sheet(isPresented: $isPresented) {
            SheetDetailView(title: "First")
                .presentationDetents([.medium, .large])
                .presentationBackgroundInteraction(.enabled)
        }
        .task {
            try? await Task.sleep(for: .seconds(0.55)) // 👈 Wait for the transition
            isPresented = true // 👈 Present the sheet
        }
    }
}

struct SecondDetailView: View {
    @State var isPresented = false // 👈 Default this to false

    var body: some View {
        VStack {
            Text("Hello Second")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.green)
        }
        .sheet(isPresented: $isPresented) {
            SheetDetailView(title: "Second")
                .presentationDetents([.medium, .large])
                .interactiveDismissDisabled()
                .presentationBackgroundInteraction(.enabled)
        }
        .task {
            try? await Task.sleep(for: .seconds(0.55)) // 👈 Wait for the transition
            isPresented = true // 👈 Present the sheet
        }
    }
}

Upvotes: 1

MatBuompy
MatBuompy

Reputation: 2093

The only way I've found to make the current sheet close and the next one open is by doing some trickery with DispatchQueses:

struct FirstDetailView: View {
    @Binding var path: NavigationPath
    @State var isPresented = true
    
    var body: some View {
        VStack {
            Text("Hello First!")
            Button("GoToSecond", action: {
                isPresented = false
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    path.append("SecondDetail")
                }
            })
            Spacer()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.yellow)
        }
        .sheet(isPresented: $isPresented, content: {
            SheetDetailView(title: "First")
                .presentationDetents([.medium, .large])
                .presentationBackgroundInteraction(.enabled)
        })
    }
}

struct SecondDetailView: View {
    
    @State var isPresented = false
    
    var body: some View {
        VStack {
            Text("Hello Second")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.green)
        }
        .sheet(isPresented: $isPresented, content: {
            SheetDetailView(title: "Second")
                .presentationDetents([.medium, .large])
                .interactiveDismissDisabled()
                .presentationBackgroundInteraction(.enabled)
        })
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                isPresented = true
            }
        }
    }
}

I've been seeing weird glitchy stuff recently on SwiftUI behalf. Let me know if this solution is good enough for you!

Upvotes: 0

Related Questions