70

With the MKMapView there's an option called "Show users current location" which will automatically show a users location on the map.

I'd like to move and zoom to this location when it's found (and if it changes).

The problem is, there doesn't appear to be any method called when the user location is updated on the map, so I have nowhere to put the code that will zoom/scroll.

Is there a way to be notified when an MKMapView has got (or updated) the user location so I can move/zoom to it? If I use my own CLLocationManager the updates I get do not correspond with the updates of the user marker on the map, so it looks silly when my map moves and zooms seconds before the blue pin appears.

This feels like basic functionality, but I've spent weeks looking for a solution and not turned up anything close.

Girish
  • 4,692
  • 4
  • 35
  • 55
Danny Tuppeny
  • 40,147
  • 24
  • 151
  • 275
  • I've added a CLLocationManager to do this manually, but it doesn't even fire at the same time as the MapView draws the user location, so it looks naff. I don't understand why this would be so difficult to do – Danny Tuppeny Mar 20 '10 at 10:14

6 Answers6

106

You have to register for KVO notifications of userLocation.location property of MKMapView.

To do this, put this code in viewDidLoad: of your ViewController or anywhere in the place where your map view is initialized.

[self.mapView.userLocation addObserver:self  
        forKeyPath:@"location"  
           options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)  
           context:NULL];

Then implement this method to receive KVO notifications

- (void)observeValueForKeyPath:(NSString *)keyPath  
                      ofObject:(id)object  
                        change:(NSDictionary *)change  
                       context:(void *)context {  

    if ([self.mapView showsUserLocation]) {  
        [self moveOrZoomOrAnythingElse];
        // and of course you can use here old and new location values
    }
}

This code works fine for me.
BTW, self is my ViewController in this context.

ddnv
  • 2,104
  • 2
  • 16
  • 16
  • 1
    I've never used KVO before, so had to Google. Looks like this will do exactly what I need - thank you! – Danny Tuppeny Apr 05 '10 at 15:26
  • 6
    There is a problem in this code. It only works if the user location is already somewhere on the visible on the map. [self.mapView isUserLocationVisible] is only true if the user location is within the current map region. Instead the line should read: self.mapView.showsUserLocation – Felix Lamouroux Jun 04 '10 at 09:20
  • @Felix that doesn't seem to be the case for me – D-Nice Jun 07 '10 at 08:37
  • 12
    For those who are confused on how to implement: [self moveOrZoomOrAnythingElse]; Just add the category at this link: http://troybrant.net/blog/2010/01/set-the-zoom-level-of-an-mkmapview/ to your code and then replace the line: [self moveOrZoomOrAnythingElse]; with [self.mapView setCenterCoordinate:self.mapView.userLocation.location.coordinate zoomLevel:14 animated:YES]; It works like a charm. – Vibhor Goyal Aug 31 '10 at 22:37
  • Thanks for awesome answer... BTW those who struggle to get it to work, you have to set "mapView.showsUserLocation = YES;" otherwise this won't work. – Prasad De Zoysa Apr 10 '13 at 15:24
  • As of iOS 5+, you should [use MKUserTrackingMode](http://stackoverflow.com/a/9903882/35690). – Senseful Apr 18 '14 at 03:17
52

This is a combination of ddnv and Dustin's answer which worked for me:

mapView is the name of the MKMapView *mapView;

In the viewDidLoad add this line, note there could be more lines in the load. This is just simplified.

- (void) viewDidLoad
{
    [self.mapView.userLocation addObserver:self 
                                forKeyPath:@"location" 
                                   options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) 
                                   context:nil];
}

Then create the actual listing method that moves the map to the current location:

// Listen to change in the userLocation
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{       
    MKCoordinateRegion region;
    region.center = self.mapView.userLocation.coordinate;  

    MKCoordinateSpan span; 
    span.latitudeDelta  = 1; // Change these values to change the zoom
    span.longitudeDelta = 1; 
    region.span = span;

    [self.mapView setRegion:region animated:YES];
}

Don't forget to dealloc properly and unregister the observer:

- (void)dealloc 
{
    [self.mapView.userLocation removeObserver:self forKeyPath:@"location"];
    [self.mapView removeFromSuperview]; // release crashes app
    self.mapView = nil;
    [super dealloc];
}
MrHus
  • 32,888
  • 6
  • 31
  • 31
  • Not correct - you can do either init/dealloc or viewDidLoad/viewDidUnload combination. viewDidLoad can be called before the dealloc occurs multiple times - the first time this happens, the app crashes. – joshis Sep 26 '12 at 23:55
47

Since iOS 5.0 Apple has added a new method to MKMapView. This method does exactly what you want and more.

Take a look at: https://developer.apple.com/documentation/mapkit/mkmapview

- (void)setUserTrackingMode:(MKUserTrackingMode)mode animated:(BOOL)animated;
Cœur
  • 37,241
  • 25
  • 195
  • 267
thijsai
  • 1,795
  • 1
  • 18
  • 26
13

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 ......) {
    } 
}

This callback should be perfectly in sync with what is displayed on the map.

yonel
  • 7,855
  • 2
  • 44
  • 51
6

Try this:

[mapView setUserTrackingMode:MKUserTrackingModeFollow animated:YES];
Kenan Karakecili
  • 731
  • 6
  • 23
1

No problem... Inside the viewDidLoad method of your UIViewController subclass that has the MKMapView add this (assuming your MKMapView is named map):

CLLocation *location = [[[CLLocation alloc] initWithLatitude:map.centerCoordinate.latitude longitude:map.centerCoordinate.longitude] autorelease]; //Get your location and create a CLLocation
MKCoordinateRegion region; //create a region.  No this is not a pointer
region.center = location.coordinate;  // set the region center to your current location
MKCoordinateSpan span; // create a range of your view
span.latitudeDelta = BASE_RADIUS / 3;  // span dimensions.  I have BASE_RADIUS defined as 0.0144927536 which is equivalent to 1 mile
span.longitudeDelta = BASE_RADIUS / 3;  // span dimensions
region.span = span; // Set the region's span to the new span.
[map setRegion:region animated:YES]; // to set the map to the newly created region
  • 3
    I tried pretty much this, but the problem seemed to be that this code runs *before* the users location has been determined. – Danny Tuppeny Mar 19 '10 at 07:20