didUpdateUserLocation not called when view for userLocation is custom

Patrick Perini picture Patrick Perini · Nov 11, 2012 · Viewed 8.8k times · Source

I have an MKMapView that's supposed to track the user's location using a custom view (not the blue dot). In order to substitute this view for the blue dot, I return it thusly:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if (annotation == [mapView userLocation])
    {
        return userLocationView;
    }
}

To initialize tracking, I call

[mapView setShowsUserLocation: YES];
[mapView setUserTrackingMode: MKUserTrackingModeFollow animated: NO];
[mapView setDelegate: self];

As would be expected, -mapView:didUpdateUserLocation: gets called once when the app loads. Unfortunately, it's never called again unless I change -mapView:viewForAnnotation: to have the following implementation:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if (annotation == [mapView userLocation])
    {
        return nil;
    }
}

With these changes, the map loads the blue dot as the indicator of the user's location, and -mapView:didUpdateUserLocation: gets called frequently, as would be expected.

Is there some sort of mutual exclusivity for tracking users' locations and have a custom user location view? How can I make both happen?

Source

This project demonstrates this issue. https://dl.dropbox.com/u/2338382/MapKitFuckery.zip

Bug

This is most likely a bug, which I've filed as a radar. In the interim, the accepted answer should prove sufficient. However, it bears noting that I had to give up entirely on [mapView userLocation] and [mapView showsUserLocation], in favor of simply a custom annotation and the CLLocationManager.

Answer

nevan king picture nevan king · Nov 14, 2012

Instead of relying on the map view's location updates, start a CLLocationManager, set its delegate and wait for -locationManager:didUpdateToLocation:fromLocation: (in iOS 5 and lower) or -locationManager:didUpdateLocations: (iOS 6). You will get much more reliable and plentiful information than using the map view's delegate methods. You probably know the way to do this, but here it is:

#import <CoreLocation/CoreLocation.h>

- (void)viewWillAppear:(BOOL)animated
{
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    [self.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
    [self.locationManager startUpdatingLocation];
}

// Deprecated in iOS 6
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{

    // Check the age of the newLocation isn't too long ago using newLocation.timestamp

    // Set the map dot using newLocation.coordinate

    // Set an MKCircle to represent accuracy using newLocation.horizontalAccuracy
}

I had a look at the delegate calls that come in to the mapView's delegate, and returning anything other than nil stops calls to -mapView:didUpdateUserLocation:, like you said. Here are the calls in the order they arrive:

 - (void)mapViewWillStartLocatingUser:(MKMapView *)mapView
 - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
 - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation
 - (void)mapViewWillStartLoadingMap:(MKMapView *)mapView
 - (void)mapView:(MKMapView *)mapView didFailToLocateUserWithError:(NSError *)error

Presumably the MKUserLocation object, not the MKMapView is the object responsible for calling the delegate with update calls. If you check the status of showsUserLocation and mapView.userLocation, they both look fine:

NSLog(@"%d %@", mapView.showsUserLocation, mapView.userLocation);

returns 1 and a non-nil object (1 <MKUserLocation: 0x1e02e580>). Maybe the mapView queries its userLocation object to get the current location, then sends it to the delegate. If that object has gone, it won't work.

It's a bit strange, but like I said, you'll get better updates from a CLLocationManager's updates.