I need to pass some bindings to a sheet that can be written to. What I've come up with works but seems a really inefficient way of doing it.
I have recreated a very simplified version of my code to use as an example.
I have a custom LocationStruct...
struct LocationStruct {
var id = UUID()
var name: String
var location: CLLocationCoordinate2D?
var haveVisited = false
}
I then have the parent view that displays a number of the LocationStruct
's - the origin, an array of waypoints and the destination...
struct ContentView: View {
@State var origin = LocationStruct(name: "Paris")
@State var waypoints = [LocationStruct(name: "Berlin"), LocationStruct(name: "Milan")]
@State var destination = LocationStruct(name: "Venice")
@State var selectedLocation: Int?
@State var showSheet = false
var body: some View {
VStack{
HStack{
Text("Origin:")
Spacer()
Text(origin.name)
}
.onTapGesture{
selectedLocation = 1000
showSheet = true
}
ForEach(waypoints.indices, id:\.self){ i in
HStack{
Text("Waypoint \(i + 1):")
Spacer()
Text(waypoints[i].name)
}
.onTapGesture{
selectedLocation = i
showSheet = true
}
}
HStack{
Text("Destination:")
Spacer()
Text(destination.name)
}
.onTapGesture{
selectedLocation = 2000
showSheet = true
}
}
.padding()
.sheet(isPresented: $showSheet){
LocationSheet(origin: $origin, waypoints: $waypoints, destination: $destination, selectedLocation: $selectedLocation)
}
}
}
I then need to read and write to the location object that was tapped on the ContentView. I'm setting selectedLocation
value to 1000 or 2000 for origin and destination otherwise its set to the waypoint array index (waypoints are limited in number so will not reach 1000).
I'm having to repeating "if let selectedLocation = ..." in quite a few places. Is there a better way of doing this, maybe some sort of computed binding or something?
struct LocationSheet: View {
@Binding var origin: LocationStruct
@Binding var waypoints: [LocationStruct]
@Binding var destination: LocationStruct
@Binding var selectedLocation: Int?
var body: some View {
VStack{
if let selectedLocation = selectedLocation {
switch selectedLocation {
case 1000:
TextField("", text: $origin.name).textFieldStyle(.roundedBorder)
case 2000:
TextField("", text: $destination.name).textFieldStyle(.roundedBorder)
default:
TextField("", text: $waypoints[selectedLocation].name).textFieldStyle(.roundedBorder)
}
}
Button(action: { markAsVisted() }){
Text("Visited")
}
}
.padding()
}
func markAsVisted(){
if let selectedLocation = selectedLocation {
switch selectedLocation {
case 1000:
origin.haveVisited = true
case 2000:
destination.haveVisited = true
default:
waypoints[selectedLocation].haveVisited = true
}
}
}
}
Thanks in advance
A trick Apple demonstrate in Data Essentials in SwiftUI WWDC 2020 at 4:18 uses a non-optional custom @State
struct that holds both isPresented
boolean and the data the sheet needs, which is only valid when the sheet is showing.
EditorConfig can maintain invariants on its properties and be tested independently. And because EditorConfig is a value type, any change to a property of EditorConfig, like its progress, is visible as a change to EditorConfig itself.
In your case it would be done as follows:
struct LocationSheetConfig {
var selectedLocation: Int = 0 // this is usually a struct, e.g. Location, or an id, e.g. UUID.
var showSheet = false
mutating func selectLocation(id: Int){
selectedLocation = id
showSheet = true
}
// usually there is another mutating func for closing the sheet.
}
@State var config = LocationSheetConfig()
.onTapGesture{
config.selectLocation(id: 1000)
}
.sheet(isPresented: $config.showSheet){
LocationSheet(origin: $origin, waypoints: $waypoints, destination: $destination, config: $config)
}
Also I noticed your Location struct is missing Identifiable
protocol conformance. And you must not use give indices to the ForEach
View, it has to be an array of Identifiable
structs, otherwise it'll crash when there is a change (because it cannot track indices, only IDs).