Using the Google Places API With MapKit

This is a post by iOS Tutorial Team Member Jason van Lint, the founder and owner of Dead Frog Studios, a boutique design and app building studio in Neuchatel, Switzerland. In the past, we wrote a tutorial on MapKit showing you how you can display your current location on the map and plot some information […] By .

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Go!(ing Places)

In order to plot pins (called annotations in iOS terminology), you first need to create a class that conforms to the MKAnnotation protocol. For more information on how this works, refer to this site’s MapKit tutorial .

Going into more detail about why you need to create this class is out of scope for this tutorial. For the time being, just trust that this class is required to pinpoint places on the map interface.

Create a new class by selecting File\New\File. Select Objective-C class and make sure it is of type NSObject by ensuring this is selected in the drop-down list. Call the class MapPoint. Make sure the two checkboxes are unselected and click Next.

Paste this into MapPoint.h, replacing its contents:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MapPoint : NSObject <MKAnnotation>
{
    
    NSString *_name;
    NSString *_address;
    CLLocationCoordinate2D _coordinate;
    
}

@property (copy) NSString *name;
@property (copy) NSString *address;
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;


- (id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate;

@end

And paste this code into MapPoint.m:

#import "MapPoint.h"

@implementation MapPoint
@synthesize name = _name;
@synthesize address = _address;
@synthesize coordinate = _coordinate;

-(id)initWithName:(NSString*)name address:(NSString*)address coordinate:(CLLocationCoordinate2D)coordinate  {
    if ((self = [super init])) {
        _name = [name copy];
        _address = [address copy];
        _coordinate = coordinate;
        
    }
    return self;
}

-(NSString *)title {
    if ([_name isKindOfClass:[NSNull class]]) 
        return @"Unknown charge";
    else
        return _name;
}

-(NSString *)subtitle {
    return _address;
}

@end

Again, if you would like to know why these files are structured this way, please refer to Ray’s excellent MapKit tutorial. For your purposes, you just need a class that will hold the name, address, and coordinates of the point you want to plot on the map.

Now that you have your MapPoint class ready, you can use it to place annotations on your map.

There’s an important new method to create first. Each time you retrieve data from Google, you need to clear any pins that may have been plotted, and draw new pins based on the new data received. This new method will handle both tasks.

Before you add this method, make sure that, when you refer to your MapPoint object, your code knows what to do with it. Go to ViewController.h and add this to the import section.

#import "MapPoint.h"

Add the new method to ViewController.m:

-(void)plotPositions:(NSArray *)data {
    // 1 - Remove any existing custom annotations but not the user location blue dot.
    for (id<MKAnnotation> annotation in mapView.annotations) {
        if ([annotation isKindOfClass:[MapPoint class]]) {
            [mapView removeAnnotation:annotation];
        }
    }
    // 2 - Loop through the array of places returned from the Google API.
    for (int i=0; i<[data count]; i++) {
        //Retrieve the NSDictionary object in each index of the array.
        NSDictionary* place = [data objectAtIndex:i];
        // 3 - There is a specific NSDictionary object that gives us the location info.
        NSDictionary *geo = [place objectForKey:@"geometry"];
        // Get the lat and long for the location.
        NSDictionary *loc = [geo objectForKey:@"location"];
        // 4 - Get your name and address info for adding to a pin.
        NSString *name=[place objectForKey:@"name"];
        NSString *vicinity=[place objectForKey:@"vicinity"];
        // Create a special variable to hold this coordinate info.
        CLLocationCoordinate2D placeCoord;
        // Set the lat and long.
        placeCoord.latitude=[[loc objectForKey:@"lat"] doubleValue];
        placeCoord.longitude=[[loc objectForKey:@"lng"] doubleValue];
        // 5 - Create a new annotation.
        MapPoint *placeObject = [[MapPoint alloc] initWithName:name address:vicinity coordinate:placeCoord];
        [mapView addAnnotation:placeObject];
    }
}

This method does a lot of stuff, so let's break it down.

Whenever you add an annotation to the map, it will be created using your MapPoint class. So to remove only the annotations you’ve added, you use a loop to go through all annotations and check to see whether the annotation is of class "MapPoint." If so, you remove it.

  1. Remove any annotations you may have drawn previously. This is a little tricky though. The pulsing blue dot that shows your current position is also an annotation. If you just did a blanket removal of all annotations, then your blue dot would disappear.

    Whenever you add an annotation to the map, it will be created using your MapPoint class. So to remove only the annotations you’ve added, you use a loop to go through all annotations and check to see whether the annotation is of class "MapPoint." If so, you remove it.

  2. The plotPositions method receives an array of values obtained from your Google API query. You now need to iterate through each item in the array and extract the information needed for each annotation.
  3. You obtain your latitude and longitude information from the "geometry" and "location" keys in the JSON data.
  4. The name and address are obtained from the "name" and "vicinity" keys. Your location coordinates need to be assigned to a special CLLocationCoordinate2D variable for storing the latitude and longitude in a way MapKit understands.
  5. Once you read all the values from the index of the array, you assign the name, address, and place coordinates to a variable of type MapPoint and call the AddAnnotation method of the mapView class, sending it this variable to plot on the map.

You need to execute this method every time you get new data from Google, so add this line to the end of the fetchedData: method:

[self plotPositions:places];

Build and run. There shouldn’t be any errors, but pressing the toolbar buttons still does nothing except show your Google results string in the output console.

What’s missing? You need another MapKit delegate method that will take your annotations as you add them using [mapView addAnnotation:placeObject], and draw them on the map.

Add this method to ViewController.m:

-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
    // Define your reuse identifier.
    static NSString *identifier = @"MapPoint";   
    
    if ([annotation isKindOfClass:[MapPoint class]]) {
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [self.mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        if (annotationView == nil) {
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
        } else {
            annotationView.annotation = annotation;
        }
        annotationView.enabled = YES;
        annotationView.canShowCallout = YES;
        annotationView.animatesDrop = YES;
        return annotationView;
    }
    return nil;    
}

This method sets up a reuse identifier named "MapPoint" and uses it to draw the pins on the map. Notice that you use some properties of the MKPinAnnotationView object you created to show animation and enable call outs when the pin is tapped.

Build and run your code again, and you should be able to click your toolbar and see points drawn on the map representing your selected place type.

There’s still one small problem. Your mapView:didAddAnnotationViews: delegate method is still set up to zoom in at your current location.

That’s OK for the initial launch of the app – you don't want to be zoomed out to see the whole world. But after that, you want the map view to always recenter on the current location using the same zoom factor you set when interacting with the map.

To put the finishing touches on your app, you’ll to create an instance variable that checks to see if this is the first launch of the app and, if not, redraws the map according to how the user has set it up.

Add the instance variable to ViewController.h below the declaration for currenDist:

    BOOL firstLaunch;

Change viewDidLoad in ViewController.m to set your instance variable to YES when you first launch the app. Add this line to the end of the method:

firstLaunch=YES;

Now change mapView:AddAnnotationViews: to look like this:

-(void)mapView:(MKMapView *)mv didAddAnnotationViews:(NSArray *)views {    
    //Zoom back to the user location after adding a new set of annotations.
    //Get the center point of the visible map.
    CLLocationCoordinate2D centre = [mv centerCoordinate];
    MKCoordinateRegion region;
    //If this is the first launch of the app, then set the center point of the map to the user's location.
    if (firstLaunch) {
        region = MKCoordinateRegionMakeWithDistance(locationManager.location.coordinate,1000,1000);
        firstLaunch=NO;
    }else {
        //Set the center point to the visible region of the map and change the radius to match the search radius passed to the Google query string.
        region = MKCoordinateRegionMakeWithDistance(centre,currenDist,currenDist);
    }
    //Set the visible region of the map.
    [mv setRegion:region animated:YES];
}

All you are doing here is checking to see if the Boolean value for firstLaunch is set to YES. If so, then you set the map up to show your current location with a zoom factor of 1000 x 1000 m.

If it is NO, then you set the visible region to where the user is currently looking at (which may be a different location than the user's position).

Build and Run your code. You should notice a default setting for the first launch, but from then on your map should always remain in the position and at the zoom level you set when you interact with the device.