Search code examples
swiftuimapkit

Select Markers of different types on SwiftUI Map


In the view described in the following code, I would like to be able to select a marker, no matter if the marker is a Place or an Accommodation. Both Place and Accommodation are defined as classes with different fields. Both of them have latitude, and longitude. But I can only make one of them selectable depending on which one I put on this line

Map(position: $cameraPosition, selection: $selectedAccommodation) 

or

Map(position: $cameraPosition, selection: $selectedAccommodation) 
import SwiftUI
import MapKit

struct DestinationMapView: View {
    var destination: Destination
    @State private var cameraPosition: MapCameraPosition = .automatic
    @State private var selectedPlace: Place?
    @State private var selectedAccommodation: Accommodation?
    //@State private var selectedPlace: SelectablePlace?
    var body: some View {
        Map(position: $cameraPosition, selection: $selectedPlace) {
            ForEach(destination.accommodations ?? []) { accommodation in
                Group {
                    if let lat = accommodation.latitude, let lon = accommodation.longitude {
                        Marker(accommodation.name, systemImage: "bed.double.fill", coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
                            .tint(.blue)
                    }
                }
                .tag(accommodation)
            }
            ForEach(destination.places ?? []) { place in
                Group {
                    if let lat = place.latitude, let lon = place.longitude {
                        Marker(place.name, systemImage: place.placeType.systemImage, coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
                    }
                }
                .tag(place)
            }
        }
        .sheet(item: $selectedPlace) { selectedPlace in
            Text("Selected Place")
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
        .sheet(item: $selectedAccommodation) { selectedAccommodation in
            Text("Selected Accommodation")
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
        .onAppear {
            if let coordinateRegion = destination.coordinateRegion {
                cameraPosition = MapCameraPosition.region(coordinateRegion)
            }
        }
    }
}

How can add the capability to select the marker no matter if it's of Place type or Accommodation type?


Solution

  • Try this approach using a dedicated Selection that holds the kind and the id of the selected Accommodation or Place. Adjust the code as required.

    enum MarkerType { // <--- here
        case place
        case accommodation
    }
    
    struct Selection: Identifiable, Hashable { // <--- here
        let id: UUID
        let kind: MarkerType
    }
    
    struct DestinationMapView: View {
        var destination: Destination
        @State private var cameraPosition: MapCameraPosition = .automatic
        @State private var selectedPlace: Place?
        @State private var selectedAccommodation: Accommodation?
        
        @State private var selection: Selection?  // <--- here
        
        var body: some View {
            Map(position: $cameraPosition, selection: $selection) {  // <--- here
                ForEach(destination.accommodations ?? []) { accommodation in
                    Group {
                        if let lat = accommodation.latitude, let lon = accommodation.longitude {
                            Marker(accommodation.name, systemImage: "bed.double.fill", coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
                                .tint(.blue)
                        }
                    }
                    .tag(Selection(id: accommodation.id, kind: .accommodation))
                }
                ForEach(destination.places ?? []) { place in
                    Group {
                        if let lat = place.latitude, let lon = place.longitude {
                            Marker(place.name, systemImage: place.placeType, coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
                        }
                    }
                    .tag(Selection(id: place.id, kind: .place))
                }
            }
            .sheet(item: $selection) { selection in  // <--- here
                if selection.kind == .accommodation {
                    Text("Selected Accommodation: \(getAccomodation(selection)?.name ?? "no name")")
                        .presentationDetents([.medium, .large])
                        .presentationDragIndicator(.visible)
                } else {
                    Text("Selected Place: \(getPlace(selection)?.name ?? "no name")")
                        .presentationDetents([.medium, .large])
                        .presentationDragIndicator(.visible)
                }
            }
            .onAppear {
                if let coordinateRegion = destination.coordinateRegion {
                    cameraPosition = MapCameraPosition.region(coordinateRegion)
                }
            }
        }
        
        // --- here
        func getAccomodation(_ selection: Selection?) -> Accommodation? {
            if let select = selection, let acc = destination.accommodations?.first(where: {$0.id == select.id})
            {
                return acc
            }
            return nil
        }
        
        func getPlace(_ selection: Selection?) -> Place? {
            if let select = selection, let place = destination.places?.first(where: {$0.id == select.id})
            {
                return place
            }
            return nil
        }
    
    }
    
    // --- for testing
    struct ContentView: View {
        let destination = Destination(accommodations: [ Accommodation(name: "acc-1", latitude: 51.515, longitude: -0.117),
                                                        Accommodation(name: "acc-2", latitude: 51.516, longitude: -0.118),
                                                        Accommodation(name: "acc-3", latitude: 51.517, longitude: -0.119)],
                                      places: [ Place(name: "place-1", latitude: 51.525, longitude: -0.127),
                                                Place(name: "place-2", latitude: 51.526, longitude: -0.128),
                                                Place(name: "place-3", latitude: 51.527, longitude: -0.129)],
                                      coordinateRegion: MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.52, longitude: -0.116), latitudinalMeters: 3500, longitudinalMeters: 3500))
        
        var body: some View {
            DestinationMapView(destination: destination)
        }
    }
    
    struct Place: Identifiable {
        let id = UUID()
        var name: String
        var latitude: Double?
        var longitude: Double?
        var placeType: String = "house" // <--- for testing
    }
    
    struct Accommodation: Identifiable {
        let id = UUID()
        var name: String
        var latitude: Double?
        var longitude: Double?
    }
    
    struct Destination: Identifiable {
        let id = UUID()
        var accommodations: [Accommodation]?
        var places: [Place]?
        var coordinateRegion: MKCoordinateRegion?
    }