How to properly put 2000+ Custom Annotations on SwiftUI Map():View to keep lag to minimum
Asked Answered
G

2

6

I was curious about what is the best practice when you need to have a bunch of custom annotations on Map in SwiftUI. This is my first real IOS project, so I'm a little rough around the edges. Currently, I have 2400 annotations, they are buttons with custom images and eventually, when the user clicks on them they will pop up information about the art piece.

What I did so far was put all the Data of each pin in a CoreData with the proper relationships to the entity it represents and an image String to call in my asset folder directly from the MapAnnotation content.

I have run it on my own iPhone 8. Fps drops significantly. I'm not sure if it's because the amount of annotations is very big, but that will not go down. Here's an example of my MapView()

FYI, I fetch the pins onAppear() and on the buttons will be a filter also. The code is far from done, just trying to fix one issue before going too far to come back.

Thanks for any input. Have a great day


import SwiftUI import MapKit import CoreData

struct MapView: View {

@Environment(\.managedObjectContext) private var moc
@State var pins: [Pin] = []
@State var userTrackingMode: MapUserTrackingMode = .follow

// Location for Montreal downtown
@State var region = MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 45.50240, longitude: -73.57067),
    span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))


var body: some View {
    
    ZStack{

        Map(coordinateRegion: $region,
            interactionModes: MapInteractionModes.all,
            showsUserLocation: true,
            userTrackingMode: $userTrackingMode,
            annotationItems: pins)
        {   pin in
            
            MapAnnotation(coordinate: CLLocationCoordinate2D(
                            latitude: pin.location?.latitude ?? 0,
                            longitude: pin.location?.longitude ?? 0))
            {
                      
                Button(action: {
                }){
                    if pin.isArtwork {
                        Image("\(pin.imageDefault!)")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 10)
                    } else if pin.isPlace {
                        Image("\(pin.imageDefault ?? "place_pin")")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 10)
                    }
                }
            }
        }
        .accentColor(Color.blue)

         HStack(alignment: .bottom){
            Spacer()
            VStack(alignment: .trailing){
                Spacer()
                Button(action: {userTrackingMode = .follow}) {
                    Image("user_location")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 40.0)
                }
                .padding()
                .padding(.bottom, -20)
                .cornerRadius(10)
                .shadow(radius: /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)


            VStack{
                Button(action: {
                    pins = fetchPins(predicate: "isArtwork == false")
                }) {
                    Image("map_filter")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 40.0)
                }
                .cornerRadius(10)
                .shadow(radius: /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
                .padding()
                .onAppear {
                    pins = fetchPins(predicate: nil)
                    }
                }
            }
            .padding(.bottom, 50)
         }
    }
}

}

extension MapView {

func fetchPins(predicate: String?) -> [Pin]{
    do {
        let request = Pin.fetchRequest() as NSFetchRequest<Pin>
        
        if predicate != nil && predicate!.count > 0 {
            let predicate = NSPredicate(format: predicate!)
            request.predicate = predicate
        }
        return try moc.fetch(request)
        
    } catch {
        fatalError("Error fetching pin + predicate")
    }
}

}


Groping answered 6/6, 2021 at 23:41 Comment(0)
S
7

I think the issue is that whenever the region changes, the view gets rebuilt, so SwiftUI ends up constantly refreshing the list of annotations.

The other answer gets rid of the lag because we no longer rebuild the view when the region changes, but I noticed that when the list of annotations updates, the map region gets reset to the initial value (which makes sense, as we're using .constant(region)). To circumvent this, I did the following.

First, create a wrapper class for the region with a custom binding.

class RegionWrapper {
    var _region: MKCoordinateRegion = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 30, longitude: -90),
        span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
    
    var region: Binding<MKCoordinateRegion> {
        Binding(
            get: { self._region },
            set: { self._region = $0 }
        )
    }
}

Now, in the view struct, create an instance of RegionWrapper and pass regionWrapper.region to the map.

This works for most cases, but if you want to change the region programmatically, you have to make a few additional changes so that the view gets rebuilt.

First, make RegionWrapper an ObservableObject, and add a @Published flag variable. The final class should look something like this:

class RegionWrapper: ObservableObject {
    var _region: MKCoordinateRegion = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 30, longitude: -90),
        span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))

    var region: Binding<MKCoordinateRegion> {
        Binding(
            get: { self._region },
            set: { self._region = $0 }
        )
    }

    @Published var flag = false
}

Update the variable in the view struct to be a @StateObject. Now, whenever you want to change the region, first set regionWrapper.region.wrappedValue, and then call regionWrapper.flag.toggle() to force the view to rebuild (you can wrap these in withAnimation for a smoother transition). Example view is below.

struct MapView: View {
    @StateObject private var regionWrapper = RegionWrapper()
    
    var body: some View {
        Map(coordinateRegion: regionWrapper.region, annotationItems: annotations) { ... }
    }
    
    func updateRegion(newRegion: MKCoordinateRegion) {
        withAnimation {
            regionWrapper.region.wrappedValue = newRegion
            regionWrapper.flag.toggle()
        }
    }
}

Hope this helps anyone who runs into this.

Spruce answered 8/1, 2022 at 0:15 Comment(1)
This worked perfectly for me, thanks!Scarce
E
5

I had a similar issue to this and found that changing the region from

coordinateRegion: $region

to

coordinateRegion: .constant(region)

stopped the lag, I could still change the region from my location manager wi

Esoterica answered 15/10, 2021 at 9:49 Comment(2)
this worked for me, since I didn't need the bidirectional binding to $region.Faldstool
Works for the lagging, but the problem occurs, when I want to do scrolling and programmatic navigation too. The bug is that after the scroll the map will jump to the programmatically selected place (e.g. annotation .ontapGesture()) not from our current map center, but first it will jump back to the last region center constant, then to the selected place. But with animation sometimes there is not enough time for that, so the animation is cancelled, and an immediate jump is carried out.Ormazd

© 2022 - 2024 — McMap. All rights reserved.