Search code examples
swiftuiswiftui-map

Custom MKMapView not reacting to location changes


In SwiftUI I’m trying to create a custom MKMapView to allow to tap on the existing map annotations.

However I’m having a hard time making the map react to the updates from the location manager. I must be doing something wrong with the states.

I’ve added a minimum reproducible case. I have two maps on the screen to show the issue. The SwiftUI Map() updates properly with the location updates, the CustomMKMapView does not.

Any idea? And thank you in advance!

import SwiftUI
import CoreLocation
import MapKit

struct CustomMapView: View {    
    @StateObject var locationMgr = LocManager()
    
    var body: some View {
        @State var region = MKCoordinateRegion(center: locationMgr.location.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
        @State var coordinate = locationMgr.location.coordinate
        
        VStack {    
            Map(coordinateRegion: $region)
                .frame(width: 400, height: 300)
            
            CustomMKMapView (
                coordinate: coordinate,
                title: "Me",
                setSelected: { 
                    print($0)
                }
            )
        }
    }
}

final class LocManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    @Published var location: CLLocation = CLLocation(latitude: 51.500685, longitude: -0.124570)
    @Published var direction: CLLocationDirection = .zero
    let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.distanceFilter = 100.0
        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
        locationManager.startUpdatingLocation()
        
        Task { [weak self] in
            try? await self?.requestAuthorization()
        }
    }
    
    func requestAuthorization() async throws {
        if locationManager.authorizationStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {        
        if let location = locations.first {
            self.location = location

            // trigger random change to make this more explicit
            let randomInt = Int.random(in: 1..<2)
            if (randomInt == 1) {
                self.location = CLLocation(latitude: 48.5307222, longitude: -0.1275)
            }
            
            print("new location \(location)")
        }
    }
}

struct CustomMKMapView: UIViewRepresentable {
    @State var coordinate: CLLocationCoordinate2D
    var title: String? = nil
    var setSelected: ((MKAnnotation?) -> Void)?
    
    var mapView: MKMapView = .init(frame: .zero)
    
    func makeUIView(context: Context) -> MKMapView {
        mapView.delegate = context.coordinator
        mapView.isUserInteractionEnabled = true
        mapView.selectableMapFeatures = [.pointsOfInterest]
        mapView.setRegion(.init(center: coordinate, span: .init(latitudeDelta: 0.02, longitudeDelta: 0.02)), animated: true)
        return mapView
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        view.addAnnotation(Annotation(title: title, coordinate: coordinate))
    }
    
    func makeCoordinator() -> MapViewCoordinator {
        MapViewCoordinator(self)
    }
    
    class MapViewCoordinator: NSObject, MKMapViewDelegate {
        var parent: CustomMKMapView
        
        init(_ parent: CustomMKMapView) {
            self.parent = parent
        }
        
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            return nil
        }
        
        func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) {
            if let annotation = annotation as? MKMapFeatureAnnotation {
                parent.setSelected?(annotation)
            }
        }
    }
    
    class Annotation: NSObject, MKAnnotation {
        let title: String?
        let coordinate: CLLocationCoordinate2D
        
        init(title: String?, coordinate: CLLocationCoordinate2D) {
            self.title = title
            self.coordinate = coordinate
        }
    }
}

struct CustomMapView_Previews: PreviewProvider {
    static var previews: some View {
        CustomMapView()
    }
}


Solution

  • Firstly, you need to remove all @State stuff, because you already had LocManager and @Published properties. @State need to be declared only in View scope and marked with private.

    struct CustomMapView: View {    
        @StateObject private var locationMgr = LocManager()
    
        var body: some View {
            //@State var region = MKCoordinateRegion(center: locationMgr.location.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
            //@State var coordinate = locationMgr.location.coordinate
            ...
    
            let coordinateBinding: Binding<CLLocationCoordinate2D> = .init {
                locationMgr.location.coordinate
            } set: { newValue in
                locationMgr.location = CLLocation(latitude: newValue.longitude,
                                                  longitude: newValue.latitude)
            }
            CustomMKMapView (
                coordinate: coordinateBinding,
                title: "Me",
                setSelected: {
                    print($0)
                }
            )
        }
    }
    

    Secondly, since both Map and CustomMKMapView receive the same coordination. And you have a single source of truth here, which is LocManager so, you don't need another @State inside CustomMKMapView, change it to @Binding.

    struct CustomMKMapView {
        @Binding var coordinate: CLLocationCoordinate2D
    }