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()
}
}
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
}