Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

didUpdateUserLocation not called when view for userLocation is custom

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.

like image 782
Patrick Perini Avatar asked Nov 11 '12 17:11

Patrick Perini


2 Answers

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.

like image 132
nevan king Avatar answered Nov 04 '22 16:11

nevan king


An old question that I would like to answer my own way. I was having an issue somewhat similar. I just needed to make a web service call, passing the user location as a GET parameter, when my MapView's ViewController/Screen was loaded and user location retrieved by the mapView. It made sense then to call the web service within the delegate method didUpdateUserLocation. I didn't notice the wrong behaviour first because things seemed to work properly but then for some reasons sometimes upon opening the mapView screen, the user blue dot would show but the map would not "zoom in" and neither didUpdateUserLocation was called nor my inner web service call obviously. After a few seconds of staring at the screen, the map would "zoom in" and the didUpdateUserLocation/web service was called. Some small glitch I thought, not a big deal. Now my detail-oriented developer's mind was still frustrated and after a few weeks of thinking this over, I decided to take action on this. Stackoverflow didn't give me the answer straight away but pointed me towards the right direction nonetheless. And here was the culprit: Sequence of calls! Maddening but made total sense once I figured things out. I knew that in order to see the blue dot and get the user location I had to tell the mapView to do so. So being an Interface Builder lover, I set things up properly for my mapView in IB, that is I checked the box: "User Location", easy. Then carefully reading the mapView documentation I realised that my ViewController needed to conform to MKMapViewDelegate, done deal. As I said, things seemed to work ok, the blue dot would show right away but sometimes the "zoom in" of the map would take a few seconds... well my iPhone is already 3 years old, things are getting slow, I'd deal with the sluggishness... Then I read this stack overflow post (https://stackoverflow.com/a/37407955) and things became clear. In my case, since using IB, here was the sequence of calls:

  1. User Location checkbox checked in IB
  2. In viewDidLoad, call: mapView.delegate = self

Whereas, here is how the sequence of calls should have been:

  1. User Location checkbox NOT checked in IB (This is important)
  2. mapView.delegate = self
  3. mapView.showsUserLocation = true

And this changes EVERYTHING. Instead of having the mapView zooming in sometimes right away and sometimes after a few seconds, the mapView now zooms in right away when the screen opens, and the didUpdateUserLocation/web service IS called.

Now a little more on the why, it still "sometimes" worked. This is simply due to the fact that the location of my iPhone was sometimes updated right after the map screen was loaded and sometimes not :-). I have to say that besides the stackoverflow post I mentioned above, what helped me too was testing my app in the Simulator since I needed to use a gpx file with obviously static location coordinate, the non predictable behaviour I described was then systematic.

So as much as I love Interface Builder, I might have to reconsider how unconditional that love is.

PS: I know this post is old and my answer not entirely related to the issue @Patrick Perini had but I wish it helps others and that it might answer @Patrick Perini's conclusion regarding the fact that a Bug was at fault when it's not. Thanks for reading this blog post :-)

like image 24
Joss Avatar answered Nov 04 '22 15:11

Joss