How can I use set up an MKMapView to only zoom in once? Then every time after it should just add annotations without zooming

Viewed 108

I want the map to open up zoomed in to the 3 London locations. Then 7 seconds later, I want the Berlin locations to show up on the map, but I don't want the map to zoom in to them. I want the map to be scrollable by the user the entire time without "jumping" anywhere. I'm guessing this may be an issue with my understanding of how SwiftUI works. But I don't understand how. Here is some code that shows what I am trying to achieve. Or you can get a working sample here -> https://github.com/cameronhenige/TestSwiftUIMapZoom. In it's current state, it zooms into the London locations, then 7 seconds later it zooms in to both the London and the Berlin locations.


import SwiftUI
import MapKit

struct ContentView: View {
    
    @ObservedObject var testViewModel = TestViewModel()
    @State var map = MKMapView()

    var SearchingButtonText: some View {
            return Text("Test" )
    }
    
    var body: some View {
            VStack {
            
                TestMapView(map: self.$map, locations: $testViewModel.locations)
                
            }.onAppear() {
                self.testViewModel.fetchData()
              }
    }
}



import Foundation
import CoreLocation
import MapKit

class TestViewModel: NSObject, ObservableObject {
    
    @Published var locations: [MKPointAnnotation] = []
    
    func fetchData() {
        
        let london = MKPointAnnotation()
        london.title = "London"
        london.coordinate = CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275)
        
        let londonTwo = MKPointAnnotation()
        londonTwo.title = "London Two"
        londonTwo.coordinate = CLLocationCoordinate2D(latitude: 51.507224, longitude: -0.1277)
        
        
        let londonThree = MKPointAnnotation()
        londonThree.title = "London Three"
        londonThree.coordinate = CLLocationCoordinate2D(latitude: 51.507226, longitude: -0.1279)
        

        let berlin = MKPointAnnotation()
        berlin.title = "Berlin"
        berlin.coordinate = CLLocationCoordinate2D(latitude: 52.5200, longitude: 13.4050)
        
        let berlinTwo = MKPointAnnotation()
        berlinTwo.title = "Berlin Two"
        berlinTwo.coordinate = CLLocationCoordinate2D(latitude: 52.5202, longitude: 13.4052)
        
        
        let berlinThree = MKPointAnnotation()
        berlinThree.title = "Berlin Three"
        berlinThree.coordinate = CLLocationCoordinate2D(latitude: 52.5204, longitude: 13.4056)
        
    
        
        locations.append(london)
        locations.append(londonTwo)
        locations.append(londonThree)
        //These should initially zoom in.
        
        let seconds = 7.0
        DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
            self.locations.append(berlin)
            self.locations.append(berlinTwo)
            self.locations.append(berlinThree)
            //These should just appear on the map but not pan the map to show them.
        }

    }
    
    
}


import SwiftUI
import MapKit

struct TestMapView: UIViewRepresentable {
    
    @Binding var map : MKMapView
    @Binding var locations: [MKPointAnnotation]
    @State var hasZoomed = false
    
    func makeUIView(context: Context) -> MKMapView {
        return map
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        self.map.addAnnotations(locations)
        
        //I only want to call this once.
        if(!hasZoomed && !self.locations.isEmpty) {
            self.map.showAnnotations(locations, animated: false)
            hasZoomed = true //here i get a warning "Modifying state during view update, this will cause undefined behavior."
        }
    }
}

1 Answers

There are a few irregularities in the code.

  1. When you create a UIViewRepresentable view, the struct is responsible for the UIKit view allocation / updating.

    The map is created as a @State and passed in, which I don't believe it's the right way to do it.

You could try something like this:

struct TestMapView: UIViewRepresentable {

    final class Coordinator {
        var hasZoomed = false
    }
    
    @Binding var locations: [MKPointAnnotation]

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    func makeUIView(context: Context) -> MKMapView {
        return MKMapView(frame: .zero)
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        view.removeAnnotations(view.annotations)
        view.addAnnotations(locations)
        if(!context.coordinator.hasZoomed && !self.locations.isEmpty) {
            view.showAnnotations(locations, animated: false)
            context.coordinator.hasZoomed = true
        }
    }
}
  1. In the view model, you may want to do this...

    locations.append(contentsOf: [london, londonTwo, londonThree])

    ...rather than appending a single location.

    Every time you append one, you fire a new event (@Published). In your scenario, it seems like you want to fire two events: the first one for the London locations, and the second one for Berlin's.

Update: I added my suggestions as a PR to your repo on Github

Related