Dogahe
Dogahe

Reputation: 1430

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?

Upvotes: 2

Views: 274

Answers (2)

Vicente Garcia
Vicente Garcia

Reputation: 6380

Starting on iOS 18 now is possible to do that with MapSelection

This allows you to select any feature on the Map (MapFeature), i.e. places of interest, and also any marker that you tag with a MapSelection.

The only requirement is that the MapSelection has to accept a value conforming to Hashable, could be any of your own types or could be as simple as an Int, String, or UUID.

Example

Using some of the code for the original post for illustration purposes (not a complete example).

Assuming that your accommodations and places both contain a unique id and that is a String, otherwise adjust as needed.

struct DestinationMapView: View {
    // ... other code
    @State private var mapSelection: MapSelection<String>? // <<<
    // ... other code

    var body: some View {
        Map(position: $cameraPosition, selection: $mapSelection) {
            ForEach(destination.accommodations ?? []) { accommodation in
                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(MapSelection(accommodation.id)) // <<<
                }
            }
            ForEach(destination.places ?? []) { place in
                if let lat = place.latitude, let lon = place.longitude {
                    Marker(place.name,
                           systemImage: place.placeType.systemImage,
                           coordinate: CLLocationCoordinate2D(
                            latitude: lat,
                            longitude: lon))
                    .tag(MapSelection(place.id)) // <<<
                }
            }
        }
        // other code
    }
}

Upvotes: 0

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?
}

Upvotes: 3

Related Questions