0

I have a View with a search button in the toolbar. The search button presents a sheet to the user and when he clicks on a result I would like the sheet to be dismissed and a detailView to be opened rather than navigating to the detailView from inside the sheet. The dismiss part is easy, but how do I open the detailView in the NavigationStack relative to the original View that presented the Sheet?

I'm also getting an error on the navigationStack initialization.

HomeScreen:

struct CatCategoriesView: View {
    
    @StateObject private var vm = CatCategoriesViewModel(service: Webservice())

    @State var showingSearchView = false
    @State var path: [CatDetailView] = []
    
    var body: some View {
        NavigationStack(path: $path) { <<-- Error here "No exact matches in call to initializer "
            ZStack {
                Theme.backgroundColor
                    .ignoresSafeArea()
                
                ScrollView {
                    switch vm.state {
                    case .success(let cats):
                        
                        LazyVStack {
                            ForEach(cats, id: \.id) { cat in
                                NavigationLink {
                                    CatDetailView(cat: cat)
                                } label: {
                                    CatCategoryCardView(cat: cat)
                                        .padding()
                                }
                            }
                        }
                        
                    case .loading:
                        ProgressView()
                        
                    default:
                        EmptyView()
                    }
                }
            }
            .navigationTitle("CatPedia")
            .toolbar {
                Button {
                    showingSearchView = true
                } label: {
                    Label("Search", systemImage: "magnifyingglass")
                }
            }
        }
        .task {
            await vm.getCatCategories()
        }
        .alert("Error", isPresented: $vm.hasError, presenting: vm.state) { detail in
            
            Button("Retry") {
                Task {
                    await vm.getCatCategories()
                }
            }
        } message: { detail in
            if case let .failed(error) = detail {
                Text(error.localizedDescription)
            }
        }
       
        .sheet(isPresented: $showingSearchView) {
            SearchView(vm: vm, path: $path)
        }
    }
}

SearchView:

struct SearchView: View {
    let vm: CatCategoriesViewModel
    
    @Environment(\.dismiss) private var dismiss
    @Binding var path: [CatDetailView]
    
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(vm.filteredCats, id: \.id) { cat in
                    Button(cat.name) {
                        dismiss()
                        path.append(CatDetailView(cat: cat))
                    }
                }
            }
            .navigationTitle("Search")
            .searchable(text: $searchText, prompt: "Find a cat..")
            .onChange(of: searchText, perform: vm.search)
        }
    }
}
Abdo23
  • 77
  • 7

1 Answers1

1

It can be a little tricky, but I'd suggest using a combination of Apple's documentation on "Control a presentation link programmatically" and shared state. To achieve the shared state, I passed a shared view model into the sheet.

I have simplified your example to get it working in a more generic way. Hope this will work for you!

ExampleParentView.swift

import SwiftUI

struct ExampleParentView: View {
    
    @StateObject var viewModel = ExampleViewModel()
    
    var body: some View {
        NavigationStack(path: $viewModel.targetDestination) {
            List {
                NavigationLink("Destination A", value: TargetDestination.DestinationA)
                NavigationLink("Destination B", value: TargetDestination.DestinationB)
            }
            .navigationDestination(for: TargetDestination.self) { target in
                switch target {
                case .DestinationA:
                    DestinationA()
                case .DestinationB:
                    DestinationB()
                }
            }
            .navigationTitle("Destinations")
            
            Button(action: {
                viewModel.showModal = true
            }) {
                Text("Click to open sheet")
            }
        }
        .sheet(isPresented: $viewModel.showModal, content: {
            ExampleSheetView(viewModel: viewModel)
                .interactiveDismissDisabled()
        })
    }
}

ExampleViewModel.swift

import Foundation
import SwiftUI

class ExampleViewModel: ObservableObject {
    @Published var showModal = false
    @Published var targetDestination: [TargetDestination] = []
}

enum TargetDestination {
    case DestinationA
    case DestinationB
}

ExampleSheetView.swift

import SwiftUI

struct ExampleSheetView: View {
    let viewModel: ExampleViewModel
    
    var body: some View {
        VStack {
            Text("I am the sheet")

            Button(action: {
                viewModel.showModal = false
                viewModel.targetDestination.append(.DestinationA)
            }) {
                Text("Close the sheet and navigate to `A`")
            }

            Button(action: {
                viewModel.showModal = false
                viewModel.targetDestination.append(.DestinationB)
            }) {
                Text("Close the sheet and navigate to `B`")
            }
        }
    }
}

DestinationA.swift

import SwiftUI

struct DestinationA: View {
    var body: some View {
        Text("Destination A")
    }
}

DestinationB.swift

import SwiftUI

struct DestinationB: View {
    var body: some View {
        Text("Destination B")
    }
}
Fraune
  • 74
  • 4