iosswiftswiftuimapkitmapview

Map annotations only appear after map is moved


I have a map which loads annotations from the Google API, when the map initially loads all the annotations they are 'placed' as seen through the print in the console, however they won't show up on the map until I move the map once. Does anyone know if I need to call a method to update the map after placing the annotations?

struct ContentView: View {
    
    var locationSearch = LocationSearch()
    @State private var mapView = MapView()
    @State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
    @EnvironmentObject var sheetManager: SheetManager

    var body: some View {
        mapView
            .popup(with: SheetManager())
            .frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
    }
    
}
struct MapView: UIViewRepresentable {
    @State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        print("FLF: MapView updated")
        uiView.setNeedsDisplay()
    }
    
    
    var locationManager = CLLocationManager()
    let mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: UIScreen.screenWidth, height: UIScreen.screenHeight))
    
    func setupManager() {
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.requestAlwaysAuthorization()
    }
    
    func makeUIView(context: Context) -> MKMapView {
        setupManager()
        mapView.region = ContentView().region
        mapView.showsUserLocation = true
        mapView.userTrackingMode = .follow
        mapView.delegate = context.coordinator // set the delegate to the coordinator
        placeMarkersForRegion(region: region)
        return mapView
    }
    
    func placeMarkersForRegion(region: MKCoordinateRegion) {
        var locationSearch = LocationSearch()
        locationSearch.performSearch(region: region) { venues in
            print("FLF: Placing \(venues.count) marker(s)")
            for marker in venues {
                let annotation = MKPointAnnotation()
                annotation.coordinate = marker.location
                annotation.title = marker.name
                mapView.addAnnotation(annotation)
            }
        }
    }
    
    func makeCoordinator() -> MapViewCoordinator {
        MapViewCoordinator(self) // pass self to the coordinator so it can call `regionDidChangeAnimated`
    }

    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        // Use the 'coordinate' property to get the current location of the map view
        let currentRegion = mapView.region
        print("FLF: Map has moved")
        self.placeMarkersForRegion(region: currentRegion)
        // Do something with the current region (e.g. update a state variable or perform a search)
    }
    
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
    var parent: MapView // add a property to hold a reference to the parent view

    init(_ parent: MapView) {
        self.parent = parent
    }

    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        // Call the parent's implementation of this method
        parent.mapView(mapView, regionDidChangeAnimated: animated)
    }

  func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
      view.canShowCallout = true
      view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

          // Get the tapped annotation
          guard let annotation = view.annotation else { return }

          // Print the title of the annotation
      print(annotation.title ?? "Unknown")
  }

  func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
      print("FLF: Marker tapped")
  }
}

Solution

  • The UIViewRepresentable and Coordinator aren't implemented correctly. E.g. makeUIView has to init it, but you are initing it as a property on the struct which is immediately lost. Also MapViewCoordinator(self) is a mistake because self, i.e. the struct, is immediately disgarded after SwiftUI has updated.

    Another issue is the @State shouldn't hold a View like how your ContentView has a @State for the MapView.

    Here is an example of how to use MKMapView with UIViewRepresentable:

    struct MKMapViewRepresentable: UIViewRepresentable {
        @Binding var userTrackingMode: MapUserTrackingMode
        
        func makeCoordinator() -> Coordinator {
            Coordinator()
        }
        
        func makeUIView(context: Context) -> MKMapView {
            context.coordinator.mapView
        }
        
        func updateUIView(_ uiView: MKMapView, context: Context) {
            // MKMapView has a strange design that the delegate is called when setting manually so we need to prevent an infinite loop
            context.coordinator.userTrackingModeChanged = nil
            uiView.userTrackingMode = userTrackingMode == .follow ? MKUserTrackingMode.follow : MKUserTrackingMode.none
            context.coordinator.userTrackingModeChanged = { mode in
                userTrackingMode = mode == .follow ? MapUserTrackingMode.follow : MapUserTrackingMode.none
            }
        }
        
        class Coordinator: NSObject, MKMapViewDelegate {
            
            lazy var mapView: MKMapView = {
                let mv = MKMapView()
                mv.delegate = self
                return mv
            }()
            
            var userTrackingModeChanged: ((MKUserTrackingMode) -> Void)?
            
            func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
                userTrackingModeChanged?(mode)
            }
        }
    }