MKMapView userTrackingMode reset in SwiftUI

1.6k Views Asked by At

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"
        }
    }
}
1

There are 1 best solutions below

4
Rob On BEST ANSWER

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.