Search code examples
swiftswiftuicore-dataswiftui-map

SwiftUI get what Object's annotation was clicked on map. Core Data


i have been trying to make so that when you press an annotation on the map that has been created by Core Data's object to show the LocationPreviewView(fish: fish). So how can i get the selected item and pass it to the LocationPreviewView() ?

I have already tried to do it by UIViewRepresentable way and already posted about it, but without success.

import SwiftUI
import MapKit
import CoreData
import CoreLocationUI
import StoreKit


struct MapMainView: View {
//???
    @State private var selectedFish: Fish?
    
    @Environment(\.managedObjectContext) private var moc
    @ObservedObject var locationManager = LocationManager.shared
    
    @State private var region = MKCoordinateRegion(center: LocationManager.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
    
    
    var body: some View {
        
        ZStack {
            Map(coordinateRegion: $region,
                interactionModes: .all,
                showsUserLocation: true,
                annotationItems: annotations) {
                MapMarker(coordinate: $0.coordinate)
            }
                .ignoresSafeArea()
            VStack(spacing: 0) {
                
                
                ZStack {
                    ForEach(fetchFish()) { fish in
// Here pass the selected fish?????????
                        LocationPreviewView(fish: fish)
                            .shadow(color: Color.black.opacity(0.3), radius: 20)
                            .padding()
                            .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
                    }
                }
            }
            
        }
    }
    var annotations: [FishAnnotation] {
        let fish = fetchFish()
        return fish.map { FishAnnotation(name: $0.title ?? "No data found", coordinate: CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.long)) }
    }
    
    func fetchFish() -> [Fish] {
        let request: NSFetchRequest<Fish> = Fish.fetchRequest()
        do {
            return try moc.fetch(request)
        } catch {
            print("Error fetching fish entities: \(error)")
            return []
        }
    }
}

struct MapMainView_Previews: PreviewProvider {
    
    @State static var mapView: MKMapView = MKMapView()
    
    static var previews: some View {
        MapMainView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}





Solution

  • As i don't know your code i used a dummy entity

    struct Fish {
        let id: UUID
        let title: String
        let lat: Double
        let long: Double    
    }
    

    and changed the FishAnnotation to:

    struct FishAnnotation: Identifiable {
        let id = UUID()
        let fish: Fish
        
        var title: String { fish.title }
        var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: fish.lat, longitude: fish.long) }
        
        init(fish: Fish) {
            self.fish = fish
        }
    }
    

    As the MapMarker doesn't conform to View i created a custom FishAnnotationView:

    struct FishAnnotationView: View {
        let title: String
    
        var body: some View {
            VStack(spacing: 0) {
                Text(title)
                    .font(.callout)
                    .padding(5)
                    .background(Color(.white))
                    .cornerRadius(10)
                
                Image(systemName: "mappin.circle.fill")
                    .font(.title)
                    .foregroundColor(.red)
                
                Image(systemName: "arrowtriangle.down.fill")
                    .font(.caption)
                    .foregroundColor(.red)
                    .offset(x: 0, y: -5)
            }
        }
    }
    

    If you want to show LocationPreviewView on the Map when the user select an annotation, you could do sth like:

    struct MapMainView: View {
        @ObservedObject var locationManager = LocationManager.shared
        
        @Environment(\.managedObjectContext) private var moc
        @State private var region = MKCoordinateRegion(center: LocationManager.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
        @State private var selectedFish: Fish?
        
        var body: some View {
            Map(coordinateRegion: $region,
                interactionModes: .all,
                showsUserLocation: true,
                annotationItems: annotations) { annotation in
                MapAnnotation(coordinate: annotation.coordinate) {
                    FishAnnotationView(title: annotation.title)
                        .onTapGesture {
                            selectedFish = annotation.fish
                        }
                }
                
                if let selectedFish = selectedFish {
                    VStack(spacing: 0) {
                        LocationPreviewView(fish: selectedFish)
                            .onTapGesture {
                                self.selectedFish = nil
                            }
                    }
                }
            }
            .ignoresSafeArea()
        }
        
        var annotations: [FishAnnotation] {
            let fish = fetchFish()
            return fish.map { FishAnnotation(fish: $0) }
        }
        
        func fetchFish() -> [Fish] {
            let request: NSFetchRequest<Fish> = Fish.fetchRequest()
            do {
                return try moc.fetch(request)
            } catch {
                print("Error fetching fish entities: \(error)")
                return []
            }
        }
    }
    

    If you want to navigate to another view, you could do sth like:

    struct MapMainView: View {
        @ObservedObject var locationManager = LocationManager.shared
        
        @Environment(\.managedObjectContext) private var moc
        @State private var region = MKCoordinateRegion(center: LocationManager.currentLocation, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
        
        var body: some View {
            Map(coordinateRegion: $region,
                interactionModes: .all,
                showsUserLocation: true,
                annotationItems: annotations) { annotation in
                MapAnnotation(coordinate: annotation.coordinate) {
                    NavigationLink {
                        LocationPreviewView(fish: annotation.fish)
                    } label: {
                        FishAnnotationView(title: annotation.title)
                    }
                }
            }
            .ignoresSafeArea()
        }
        
        var annotations: [FishAnnotation] {
            let fish = fetchFish()
            return fish.map { FishAnnotation(fish: $0) }
        }
        
        func fetchFish() -> [Fish] {
            let request: NSFetchRequest<Fish> = Fish.fetchRequest()
            do {
                return try moc.fetch(request)
            } catch {
                print("Error fetching fish entities: \(error)")
                return []
            }
        }
    }
    

    Feel free to modify to fit your needs.