swiftuimapkitmapkitannotation

How to generate annotations on a certain zoom and only those close to where user zoomed in?


My app requests JSON data (latitude, longitude, and other information about a place) and then displays them on a map in a form of clickable annotations. I'm receiving around 30,000 of those, so as you can imagine, the app can get a little "laggy".

The solution I think would fit the app best is to show those annotations only on a certain zoom level (for example when the user zooms so only one city is visible at once, the annotations will show up). Since there's a lot of them, showing all 30,000 would probably crash the app, that's why I also aim at showing just those that are close to where the user zoomed in.

The code below shows immediately all annotations at once at all zoom levels. Is there a way to adapt it to do the things I described above?

struct Map: UIViewRepresentable {
    
    @EnvironmentObject var model: ContentModel
    @ObservedObject var data = FetchData()
    
    var locations:[MKPointAnnotation] {
        
        var annotations = [MKPointAnnotation]()
        
        // Loop through all places
        for place in data.dataList {
            
            // If the place does have lat and long, create an annotation
            if let lat = place.latitude, let long = place.longitude { 
                
                // Create an annotation
                let a = MKPointAnnotation()
                a.coordinate = CLLocationCoordinate2D(latitude: Double(lat)!, longitude: Double(long)!)
                a.title = place.address ?? ""
                
                annotations.append(a)
                
            }
        }
        
        return annotations
        
    }
    
    func makeUIView(context: Context) -> MKMapView {
        
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        
        // Show user on the map
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .followWithHeading
        
        return mapView
        
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        
        // Remove all annotations
        uiView.removeAnnotations(uiView.annotations)
        
        // HERE'S WHERE I SHOW THE ANNOTATIONS
        uiView.showAnnotations(self.locations, animated: true)
        
    }
    
    static func dismantleUIView(_ uiView: MKMapView, coordinator: ()) {
        
        uiView.removeAnnotations(uiView.annotations)
        
    }
    
    
    // MARK: Coordinator Class
    
    func makeCoordinator() -> Coordinator {
        
        return Coordinator(map: self)
        
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {
        
        var map: Map
        
        init(map: Map) {
            
            self.map = map
            
        }
        
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            
            // Don't treat user as an annotation
            if annotation is MKUserLocation {

                return nil

            }
            
            // Check for reusable annotations
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: Constants.annotationReusedId)
            
            // If none found, create a new one
            if annotationView == nil {
                
                annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: Constants.annotationReusedId)
                
                annotationView!.canShowCallout = true
                annotationView!.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
                
            } else {
                
                // Carry on with reusable annotation
                annotationView!.annotation = annotation
                
            }
            
            return annotationView
            
        }
    }
}

Been searching for an answer for a while now and found nothing that worked well. I imagine there's a way to get visible map rect and then condition that in Map struct, but don't know how to do that. Thanks for reading this far!


Solution

  • Your delegate can implement mapView(_:regionDidChangeAnimated:) to be notified when the user finishes a gesture that changes the map's visible region. It can implement mapViewDidChangeVisibleRegion(_:) to be notified while the gesture is happening.

    You can get the map's visible region by asking it for its region property. Regarding zoom levels, the region documentation says this:

    The region encompasses both the latitude and longitude point on which the map is centered and the span of coordinates to display. The span values provide an implicit zoom value for the map. The larger the displayed area, the lower the amount of zoom. Similarly, the smaller the displayed area, the greater the amount of zoom.

    Your updateUIView method recalculates the locations array every time SwiftUI calls it (because locations is a computed property). You should check how often SwiftUI is calling updateUIView and decide whether you need to cache the locations array.

    If you want to efficiently find the locations in the visible region, try storing the locations in a quadtree.