Search code examples
iosswiftuimapkitmkusertrackingmode

MKMapView userTrackingMode reset in SwiftUI


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

Solution

  • 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.