5

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"
        }
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044

1 Answers1

12

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • This did solve the problem. Why? Who knows. Thanks. – Nick Nov 10 '20 at 17:30
  • Strange and many thanks for going through this nightmare for me :-) Two more things to notice: (1) the `showsUserLocation` is always switched to `true` when activating `userTrackingMode` (somehow makes sense) and (2) if `isUserInteractionEnabled` is `true`, then `MKMapView` continues to switch `userTrackingMode` to `.none`. It seems to be impossible to have both at the same time. Result for me: I do not use `userTrackingMode` any more but rather use `setCenter()` and simulate tracking. – jboi Sep 11 '22 at 14:49
  • Unfortunately, the solution didn't work for me, maybe someone else has found another solution? – sandorb Sep 28 '22 at 09:50
  • FWIW, i had to have `let mapView = MKMapView(frame: UIScreen.main.bounds)` AND `DispatchQueue.main.async` in `updateUIView` for this to work with `.follow` – tjb Aug 07 '23 at 17:02