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?