swiftui

SwiftUI - Passing bindings to sheet view


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


Solution

  • 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).