MKMapView's user location is wrong on startup or resume

7.3k views Asked by At

When I start my application fresh, or resume after a long time, MKMapView's notion of the userLocation is wrong and shows me in the middle of the sea.

I am using the following code:

self.mapView.centerCoordinate = self.mapView.userLocation.location.coordinate;
[mapView setCenterCoordinate:self.mapView.userLocation.location.coordinate zoomLevel:ZOOM_LEVEL animated:YES];

Happens after a lengthy resume of the app or brand new start....

7

There are 7 answers

2
Jeremy Massel On

This is unfortunately a function of the GPS chip. It's not always on, so the data will usually be wrong for the first couple of moments. Your best bet would probably be to store the last position recorded by the app in NSUserDefaults, then wait for the precision to be where you want it to be before switching to live data, or else hide the MkMapView until the precision is where you want it to be, then display it at that point (you could show a loading screen in the interim)

1
Nitrex88 On

How far off is it from your actual location? When an application just starts or resumes from a long period of time the user location is general always wrong to begin with. As the application runs it will get more and more accurate. There are methods for getting the accuracy of the user location in CLLocationManager, see what kind of values you get there.

0
AudioBubble On

Just as Jeremy Massel wrote you have to sort out the first bad positions on app start / continue. Found a great blog post a couple of months ago:

http://troybrant.net/blog/2010/02/detecting-bad-corelocation-data/

0
Guntis Treulands On

In my apps I used these methods:

First - it's good to have a default position (for example - application is about a city - then zoom in to show whole city) (on first - first app opening, when no data downloaded)

Second - if user has already used map - can store his coordinates, and show those (+ span(zoom) level) in case there are no other data available.

Third - in case of data:

1.) If database has already some coordinates - on first map opening (when application has been closed) - zoom out/in to show all pins on map.

2.) When still in map and just arrived updates (new locations or changed something), there are two possibilities :

2.1.) If user has not zoomed in to any location - map zooms in/out to show all locations.

2.2.) if user has zoomed in - locations are updated, but map doesnt zoom out

Ofcourse - then there are also some filter buttons (for example : search, user location, and some more - which filters locations, and zooms out/in to show filtered results)

1
Ved On

What I do here is-

  1. start location updates
  2. on location update, check the age of location, if it's too old, I wait
  3. on receiving new update I update it on the map

If you need very accurate location, then put a check on accuracy as well and a timeout beyond which you can't tolerate waiting and use the less accurate fix.

1
yonel On

That's the expected behavior : the user location isn't always tracked by the iPhone using GPS (it would consume to much battery). So as soon as the map is displayed, the MKMapView instance shows the last 'best' user position it knows and then, improves the accuracy by activating the tracking (this is a seamless process, you don't have to care about it) .

You can monitor when the MKMapView updates the user location on the map by implementing the MKMapViewDelegate protocol. Just implement :

- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation      {
    CLLocationAccuracy accuracy = userLocation.location.horizontalAccuracy;
    if (accuracy ......) {
    } 
}

(More info from the apple documentation here )

The code above in my example checks the accuracy of the position currently being displayed by the mapView and reacts accordingly.

[EDIT]
If showing the user location in the middle of the sea at first really bother you, you can you hide the user location until you get a location that is accurate/fresh enough.
To do so, set the showsUserLocation property of the MKMapView to NO at first until you get an accurate enough location (thanks to the previous delegate callback) and then set it to YES.
By doing you, you will avoid displaying a location that is not accurate or too old to be diplayed (there is a timestamp property in the CLLocation to check wether it's an old location or not)

N.B:You don't have to create a CLLocationManager instance on your side, the MKMapView creates one internally and publish locations it receives via this delegate selector.

1
progrmr On

When using CLLocationManager directly, you normally get a cached location in the first callback. It's normally a good location, although old. After that you quickly get additional callbacks giving better locations using wifi, cell tower (if available). If you have asked for < 1000m accuracy you will (after more seconds) get GPS triangulation.

None of those should be inaccurate enough to be in the middle of the ocean. I suspect that the this line of code:

self.mapView.centerCoordinate = self.mapView.userLocation.location.coordinate;

is accessing the coordinate while userLocation or location is nil. If userLocation or location is nil, this will return 0 coordinates. The location of lat=0, lon=0 is in the Atlantic Ocean, off the coast of Africa. You could add a check of location to make sure it is not nil before getting the coordinate from it, ie:

if (self.mapView.userLocation.location) {
    self.mapView.centerCoordinate = self.mapView.userLocation.location.coordinate;
    [mapView setCenterCoordinate:self.mapView.userLocation.location.coordinate zoomLevel:ZOOM_LEVEL animated:YES];           
}

You will also want to wait for callbacks to the MKMapViewDelegate mapView:didUpdateUserLocation: to know when there is a valid location available. Your implementation of didUpdateUserLocation: should discard any location that has a horizontalAccuracy < 0 which indicates an invalid location.

-(void)mapView:(MKMapView*)mapView didUpdateUserLocation:(MKUserLocation*)userLocation
{
    if (userLocation.location.horizontalAccuracy > 0) {
        [mapView setCenterCoordinate:self.mapView.userLocation.location.coordinate zoomLevel:ZOOM_LEVEL animated:YES];           
    }
}