GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Is there a way to use NavigationStack(path:) and navigationDestination(for:, destination:) but open different types of views?

Forums > SwiftUI

I want to do something similar to what Paul shows us how to do in his iTour project in his SwiftData book.

Basically, his app simply uses SwiftData to store destinations that the user would like to visit. It's pretty simple, and just shows a list of all of the existing destinations. Then, there is an "Add" button to add a destination, and it automatically takes you to an EditDestinationView where you can edit the details of a destination. When you tap on a destination in the list, it also takes you to an EditDestinationView for the destination that you tapped on.

import SwiftData
import SwiftUI

struct ContentView: View {
    @Environment(\.modelContext) var modelContext

    @State private var path = [Destination]()
    @State private var sortOrder = [SortDescriptor(\Destination.name), SortDescriptor(\Destination.priority)]
    @State private var dateFilterIsOn = false
    @State private var searchText = ""

    var body: some View {
        NavigationStack(path: $path) {
            DestinationListingView(dateFilterIsOn: dateFilterIsOn, sort: sortOrder, searchString: searchText)
                .navigationTitle("iTour")
                .navigationDestination(for: Destination.self, destination: EditDestinationView.init)
                .searchable(text: $searchText)
                .toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button("Add Destination", systemImage: "plus", action: addDestination)
                    }

                    ToolbarItem(placement: .topBarLeading) {
                        Menu("Sort", systemImage: "arrow.up.arrow.down") {
                            Picker("Sort", selection: $sortOrder) {
                                Text("Name")
                                    .tag([SortDescriptor(\Destination.name), SortDescriptor(\Destination.date)])

                                Text("Priority")
                                    .tag([SortDescriptor(\Destination.priority, order: .reverse), SortDescriptor(\Destination.date)])

                                Text("Date")
                                    .tag([SortDescriptor(\Destination.date), SortDescriptor(\Destination.priority, order: .reverse)])
                            }
                            .pickerStyle(.inline)
                        }
                    }

                    ToolbarItem(placement: .topBarLeading) {
                        Menu("Filter", systemImage: "calendar.badge.clock") {
                            Picker("Filter", selection: $dateFilterIsOn) {
                                Text("Show All")
                                    .tag(false)

                                Text("Show Upcoming")
                                    .tag(true)
                            }
                            .pickerStyle(.inline)
                        }
                    }
                }
        }
    }

    func addSamples() {
        let rome = Destination(name: "Rome")
        let florence = Destination(name: "Florence")
        let naples = Destination(name: "Naples")

        modelContext.insert(rome)
        modelContext.insert(florence)
        modelContext.insert(naples)
    }

    func addDestination() {
        let destination = Destination()
        modelContext.insert(destination)
        path = [destination]
    }
}

We separated the view that actually lists the destinations from this view into DestinationListingView, in order to allow us to pass in SortDescriptors and make the list sorting dynamic. So, we end up with a view like this.

import SwiftData
import SwiftUI

struct DestinationListingView: View {
    @Environment(\.modelContext) var modelContext

    @Query(sort: [SortDescriptor(\Destination.priority, order: .reverse), SortDescriptor(\Destination.name)]) var destinations: [Destination]

    var body: some View {
        List {
            ForEach(destinations) { destination in
                NavigationLink(value: destination) {
                    VStack(alignment: .leading) {
                        Text(destination.name)
                            .font(.headline)

                        Text(destination.date.formatted(date: .long, time: .shortened))
                            .font(.subheadline)
                    }
                }
            }
            .onDelete(perform: deleteDestinations)
        }
    }

    init(dateFilterIsOn: Bool, sort: [SortDescriptor<Destination>], searchString: String) {

        let currentDate = Date.now

        if dateFilterIsOn {
            _destinations = Query(filter: #Predicate {
                if searchString.isEmpty {
                    return $0.date > currentDate
                } else {
                    return $0.name.localizedStandardContains(searchString) && $0.date > currentDate
                }
            }, sort: sort)

        } else {
            _destinations = Query(filter: #Predicate {
                if searchString.isEmpty {
                    return true
                } else {
                    return $0.name.localizedStandardContains(searchString)
                }
            }, sort: sort)

        }
    }

    func deleteDestinations(_ indexSet: IndexSet) {
        for index in indexSet {
            let destination = destinations[index]
            modelContext.delete(destination)
        }
    }
}

So, I think this line in DestinationListingView...

NavigationLink(value: destination)

causes a spcific destination to be sent back to this line in ContentView when one is tapped...

NavigationStack(path: $path) {

and then, since we have this line of code in ContentView

.navigationDestination(for: Destination.self, destination: EditDestinationView.init)

it causes the EditDestinationView for the destination that was passed in to be opened on the screen.

However, in my app, I would like to have something like an EditDestinationView open when the "Add" button is tapped, but have a DestinationView open instead when an item from the list is tapped.

I tried experimenting with creating...

 @State private var editModeIsOn

in ContentView, and then modifying this line to

.navigationDestination(for: Destination.self, destination: editModeIsOn ? EditDestinationView.init : DestinationView.init)

But that gives me an error saying "Type 'Any' cannot conform to 'View'". So, I guess I can't use a ternary operator with two different types of views with this modifier. Does anybody have any other ideas of how I could accomplish this?

   

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. Unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a FREE crash course.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.