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 Episode Source Code Foursquare Developer Center - If you want to follow along, you'll need to create a Foursquare app Location Awareness Programming Guide - The starting place to learn about CoreLocation and MapKit MKAnnotation Protocol Reference 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