I’m having troubles showing a MKMapView
in SwiftUI with userTrackingMode
set to .follow
. I’m showing a map with:
struct ContentView: View {
var body: some View {
MapView()
}
}
And in this MapView
I’m (a) setting userTrackingMode
and (b) making sure I’ve got when-in-use permissions. I do this sort of pattern all the time in storyboard-based projects. Anyway, the MapView
now looks like:
final class MapView: UIViewRepresentable {
private lazy var locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
let mapView = MKMapView()
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow // no better is mapView.setUserTrackingMode(.follow, animated: true)
return mapView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print(#function, uiView.userTrackingMode)
}
}
Everything looks good here, but the map (on both simulator and physical device) is not actually in follow-user tracking mode.
So, I expanded upon the above to add to add a coordinator that adopts MKMapViewDelegate
protocol, so I can watch what’s happening to the tracking mode:
final class MapView: UIViewRepresentable {
private lazy var locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow // no better is mapView.setUserTrackingMode(.follow, animated: true)
return mapView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print(#function, uiView.userTrackingMode)
}
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator(self)
}
}
class MapViewCoordinator: NSObject {
var mapViewController: MapView
var token: NSObjectProtocol?
init(_ control: MapView) {
self.mapViewController = control
}
}
extension MapViewCoordinator: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
print(#function, mode)
}
}
That results in:
mapView(_:didChange:animated:) MKUserTrackingMode.follow updateUIView(_:context:) MKUserTrackingMode.follow mapView(_:didChange:animated:) MKUserTrackingMode.none
There’s something going on that is resetting the userTrackingMode
to .none
.
For giggles and grins, I tried resetting userTrackingMode
, and that is no better:
func updateUIView(_ uiView: UIViewType, context: Context) {
print(#function, uiView.userTrackingMode)
uiView.userTrackingMode = .follow
}
This kludgy pattern does work, though:
func updateUIView(_ uiView: UIViewType, context: Context) {
print(#function, uiView.userTrackingMode)
DispatchQueue.main.async {
uiView.userTrackingMode = .follow
}
}
Or anything that resets the userTrackingMode
later, after this initial process, also appears to work.
Am I doing something wrong with UIViewRepresentable
? A bug in MKMapView
?
It’s not really relevant, but this is my routine to display the tracking modes:
extension MKUserTrackingMode: CustomStringConvertible {
public var description: String {
switch self {
case .none: return "MKUserTrackingMode.none"
case .follow: return "MKUserTrackingMode.follow"
case .followWithHeading: return "MKUserTrackingMode.followWithHeading"
@unknown default: return "MKUserTrackingMode unknown/default"
}
}
}
Infuriatingly, after spending an inordinate amount of time debugging this, preparing the question, etc., it looks like this strange behavior only manifests itself if you don’t supply a frame
during initialization:
let mapView = MKMapView()
When I used the following (even though the final map is not this size), it worked correctly:
let mapView = MKMapView(frame: UIScreen.main.bounds)
I’ll still post this in the hopes that it saves someone else from this nightmare.