Episode #64

MapKit Annotations

14 minutes
Published on April 25, 2013

This video is only available to subscribers. Get access to this video and 572 others.

In this episode I continue our WhatsAround sample from Episode 63. Using the Foursquare API, we fetch coffee shops near the user's location and display a pin on the map for each using the MKAnnotation protocol.

Episode Links

Adding your Foursquare API Keys

Before starting, open up the WhatsAround-Prefix.pch file and enter in your Foursquare Client ID and Secret. If you don't have these, the API calls will not work.

Fetching venues for a location using Foursquare's API

We're using the venue search API, since it returns a simplified list of venues, which is good for our needs. Each Foursquare call needs to have a client_id, client_secret, and v parameters, so to avoid duplication, we override the requestWithMethod:path:parameters method provided by AFNetworking to customize the NSURLRequest that is created. We can simply merge the passed in parameters with these common ones to make all calls use these params:

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                      path:(NSString *)path
                                parameters:(NSDictionary *)parameters {
    NSMutableDictionary *params = [parameters mutableCopy];
    [params setObject:FOURSQUARE_APP_CLIENT_ID forKey:@"client_id"];
    [params setObject:FOURSQUARE_APP_CLIENT_SECRET forKey:@"client_secret"];

    // versioning parameter, expected to contain the hard coded date the API was verified.
    // This is per Foursquare's API guide.  Must be in YYYYMMDD format.
    [params setObject:@"20130420" forKey:@"v"];

    return [super requestWithMethod:method path:path parameters:params];
}

Then we can focus on each API calls specific params. Here is how we can search for venues near a location:

- (void)fetchVenuesNear:(CLLocationCoordinate2D)coordinates
             searchTerm:(NSString *)searchTerm
         radiusInMeters:(CGFloat)radius
             completion:(FSQVenuesBlock)completion {
    id params = @{
                  @"ll" : [self latLongValueForCoordinate:coordinates],
                  @"radius" : @(radius),
                  @"query" : searchTerm,
                  @"intent" : @"browse"
                };
    [self getPath:@"venues/search"
       parameters:params
          success:^(AFHTTPRequestOperation *operation, id responseObject) {
              NSArray *venues = [self venuesForResponse:responseObject[@"response"][@"venues"]];
              completion(venues, nil);

          } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
              NSLog(@"Response status code: %d", operation.response.statusCode);
              NSLog(@"Response body: %@", operation.responseString);
              NSLog(@"ERROR: %@", error);
              completion(nil, error);
          }];
}

- (NSArray *)venuesForResponse:(NSArray *)venueDictionaries {
    NSMutableArray *venues = [NSMutableArray arrayWithCapacity:[venueDictionaries count]];
    for (id venueDictionary in venueDictionaries) {
        [venues addObject:[FSQVenue venueWithDictionary:venueDictionary]];
    }
    return venues;
}

- (NSString *)latLongValueForCoordinate:(CLLocationCoordinate2D)coord {
    return [NSString stringWithFormat:@"%g,%g", coord.latitude, coord.longitude];
}

Getting the user's location

We can get the user's location with CoreLocation using a CLLocationManager. After finding the location, we stop updating, since we don't need to continuously monitor location changes.

We need to declare a couple of properties as well as conform to the CLLocationManagerDelegate protocol so we can receive the callbacks for location changes:

@interface WARMapViewController () <CLLocationManagerDelegate>

@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSArray *venues;

@end

When the view loads we start monitoring location:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self updateLocation];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

    self.locationManager = nil;
}

- (CLLocationManager *)locationManager {
    if (!_locationManager) {
        _locationManager = [[CLLocationManager alloc] init];
        _locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
        _locationManager.delegate = self;
    }
    return _locationManager;
}

- (void)updateLocation {
    [self.locationManager startUpdatingLocation];
}

We then implement the requisite delegate methods to fetch venues for the user's location:

- (void)zoomToLocation:(CLLocation *)location radius:(CGFloat)radius {
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(location.coordinate, radius * 2, radius * 2);
    [self.mapView setRegion:region animated:YES];
}

#pragma mark - CLLocationManagerDelegate methods

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
    CLLocation *location = [locations lastObject];
    [self fetchVenuesForLocation:location];
    [self zoomToLocation:location radius:2000];

    [self.locationManager stopUpdatingLocation];
}

Next we need to implement the fetchVenuesForLocation: method:

- (void)fetchVenuesForLocation:(CLLocation *)location {
    [SVProgressHUD show];
    [[FSQFoursquareAPIClient sharedClient] fetchVenuesNear:location.coordinate
                                                searchTerm:@"coffee"
                                            radiusInMeters:4000
                                                completion:^(NSArray *venues, NSError *error) {
                                                    if (error) {
                                                        [SVProgressHUD showErrorWithStatus:@":("];
                                                    } else {
                                                        [SVProgressHUD dismiss];
                                                        self.venues = venues;
                                                        [self updateAnnotations];
                                                    }
                                                }];
}

- (void)updateAnnotations {
    for (FSQVenue *venue in self.venues) {
        WARVenueAnnotation *annotation = [[WARVenueAnnotation alloc] initWithVenue:venue];
        [self.mapView addAnnotation:annotation];
    }
}

Our annotation class is a simple adapter class that thinly wraps FSQVenue with something that looks like an annotation:

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

@interface WARVenueAnnotation : NSObject <MKAnnotation>

- (id)initWithVenue:(FSQVenue *)venue;

@end

@interface WARVenueAnnotation ()
@property (nonatomic, strong) FSQVenue *venue;
@end

@implementation WARVenueAnnotation

- (id)initWithVenue:(FSQVenue *)venue {
    self = [super init];
    if (self) {
        self.venue = venue;
    }
    return self;
}

- (CLLocationCoordinate2D)coordinate {
    return CLLocationCoordinate2DMake([self.venue.latitude floatValue], [self.venue.longitude floatValue]);
}

- (NSString *)title {
    return self.venue.name;
}

@end