Augmented Reality iOS Tutorial: Location Based

Jean-Pierre Distler Jean-Pierre Distler
Learn how to make a location based augmented reality app that displays points of interest over video!

Learn how to make a location based augmented reality app that displays points of interest over video!

Augmented reality is a cool and popular technique where you view the world through a device (like your iPhone camera, or Google Glass), and the device overlays extra information on top of the real-world view.

I’m sure you’ve seen marker tracking iOS apps where you point the camera at a marker and a 3D image pops out. And of course, you’re probably familiar with the robotic overlays in Terminator!

In this augmented reality iOS tutorial, you will write an app that takes the user’s current position and identifies nearby points of interest (we’ll call these POIs). You’ll add these points to a MapView and display them also as overlays on a camera view.

To find the POIs, you’ll use Google’s Places API, and you’ll use the AR Toolkit to show the POIs on the camera view and calculate the distance from the user’s current position.

This tutorial assumes you have some basic familiarity with MapKit. If you are completely new to MapKit, check out our Introduction to MapKit tutorial.

Getting Started

First download the starter project. This project includes the needed frameworks, the MapView and has all the necessary things already hooked up for you, so that you can get right to adding the augmented reality portion of the app.

StarterProject

Before you can do anything else, you need to obtain the user’s current location. For this, you’ll use the Core Location framework. Open MainViewController.m and add the following right after the MapKit import:

#import <CoreLocation/CoreLocation.h>

Next find the class extension directly under the imports and add the CLLocationManagerDelegate to the protocol list. This tells the compiler that your view controller implements this protocol and will be the delegate for the CLLocationManager.

Now you need a property for the MKMapView and the CLLocationManager. There’s no need to add it to the public interface, so you’ll also do it in the class extension. Add the property after your mapView so that your extension looks like this:

@interface MainViewController () <CLLocationManagerDelegate, MKMapViewDelegate>
 
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (nonatomic, strong) CLLocationManager *locationManager;
 
@end
Note: Declaring properties and protocols to a private extension helps keep your objects clean when being reused throughout your projects. It is a great habit to get into.

Now that everything is prepared, you can get the location. First you need to create and set up the CLLocationManager. To do this, open MainViewController.m and replace viewDidLoad with the following:

- (void)viewDidLoad {
	[super viewDidLoad];
 
	[self setLocationManager:[[CLLocationManager alloc] init]];
	[_locationManager setDelegate:self];
	[_locationManager setDesiredAccuracy:kCLLocationAccuracyNearestTenMeters];
	[_locationManager startUpdatingLocation];
}

The first line that you added creates the LocationManager and stores it in the property you added above. The next lines configure the manager.

The manager needs a delegate to notify when it has updated the position of the iDevice. You set it to your view controller using self. Then the manager needs know how accurate the position should be. You set it to kCLLocationAccuracyNearestTenMeters, which will be accurate enough for this example project. The last line starts the manager.

Note: For desiredAccuracy, you should use the lowest accuracy that is good enough for your purposes. Why?

Let’s say you only need an accuracy of some hundred meters – then the LocationManager can use phone cells and WLANs to get the position. This saves battery life, which you know is a big limiting factor on iDevices. But if you need a better determination of the position, the LocationManager will use GPS, which drains the battery very fast. This is also why you should stop updating the position as soon as you have an acceptable value.

Now you need to implement a delegate method to get the current location. Add the following code to MainViewController.m:

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
	//1
	CLLocation *lastLocation = [locations lastObject];
 
	//2
	CLLocationAccuracy accuracy = [lastLocation horizontalAccuracy];
	NSLog(@"Received location %@ with accuracy %f", lastLocation, accuracy);
 
	//3
	if(accuracy < 100.0) {
		//4
		MKCoordinateSpan span = MKCoordinateSpanMake(0.14, 0.14);
		MKCoordinateRegion region = MKCoordinateRegionMake([lastLocation coordinate], span);
 
		[_mapView setRegion:region animated:YES];
 
		// More code here
 
		[manager stopUpdatingLocation];
	}
}

Let’s walk through this method step-by-step:

  1. Every time the LocationManager updates the location, it sends this message to its delegate, giving it the updated locations. The locations array contains all locations in chronological order, so the newest location is the last object in the array. That’s what you need and what the first line is taking from the array.
  2. The next line gets the horizontal accuracy and logs it to the console. This value is a radius around the current location. If you have a value of 50, it means that the real location can be in a circle with a radius of 50 meters around the position stored in lastLocation.
  3. The if statement checks if the accuracy is high enough for your purposes. I chose a value of 100 meters. It is good enough for this example and you don’t have to wait too long to achieve this accuracy. In a real app, you would probably want an accuracy of 10 meters or less, but in this case it could take a few minutes to achieve that accuracy (GPS tracking takes time).
  4. The first three lines zoom the MapView to the location. After that, you stop updating the location to save battery life.

Build and run on your device, and keep your eyes on the console to see how the locations come in and how the accuracy gets better and better. Eventually you’ll see the map zoom to an area centered on your current location.

Note: There is also a property called verticalAccuracy that is the same as horizontalAccuracy, except that it’s for the altitude of the position. So a value of 50 means that the real altitude can be 50 meters higher or lower. For both properties, negative values are invalid.

Adding Google Places

Now that you have a current location, you can load a list of POIs. To get this list, you’ll use Google’s Places API.

Google Places API requires you to register for access. If you’ve already created a Google account in the past to access APIs like Maps, go here and select Services. Then skip the following steps until you reach Enabling the Places API.

However, if you’ve never used Google Places API before, you’ll need to register for an account.

Google Register

You can skip the second screen and on the third, click on Back to Developer Consoles.

Bildschirmfoto 2013-06-07 um 09.01.03

Now click on Create Project and you will see the Developer Console. To enable the Places API, search for the line Places API and click on the switch to turn it on. You must enter a company name and website to activate the API.

Bildschirmfoto 2013-06-07 um 09.06.40

In the next step, review the license agreement and make sure you’re OK with the terms.

Bildschirmfoto 2013-06-07 um 09.07.29

Now you can get your API Key by clicking on the button API Access on the left side. Copy the value next to API key and save it to your disk. You will need it in a few moments.

API-Key

Defining PlacesLoader

To handle the networking, you will create a new class that does everything for you. Go to File\New\New File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next.

PlacesLoader1

Call your new class PlacesLoader, make it a subclass of NSObject and click Next.

PlacesLoader2

Decide where you want to save your class files and then click Create. Make sure that your project target is checked so that you add your new class to the target.

Screen Shot 2013-07-27 at 5.50.21 PM

Open PlacesLoader.h and change the code to the following:

#import <Foundation/Foundation.h>
 
//1
@class CLLocation;
 
//2
typedef void (^SuccessHandler)(NSDictionary *responseDict);
typedef void (^ErrorHandler)(NSError *error);
 
@interface PlacesLoader : NSObject
 
//3
+ (PlacesLoader *)sharedInstance;
 
//4
- (void)loadPOIsForLocation:(CLLocation *)location radius:(int)radius successHandler:(SuccessHandler)handler errorHandler:(ErrorHandler)errorHandler;
 
@end

Here’s what this code is doing:

  1. The PlacesLoader works with CLLocation objects, so you tell the compiler that a class with this name will exist without importing any libraries. Remember that wherever you would import PlacesLoader.h, you would then also import Core Location.
  2. PlacesLoader uses NSURLConnections for the networking and blocks to give the response to the class that invoked the request. To make this a little easier and more readable, there are two typedefs for the blocks. The first is a block that will be invoked when everything works successfully. The block has no return value and takes a NSDictionary as its only parameter. The dictionary contains the parsed JSON-response from Google. The second block is invoked in case an error occurs and has an NSError instance as parameter.
  3. PlacesLoader will be a Singleton class, so you have a sharedInstance method that will give you the instance.
  4. There is also a method that starts loading POIs from Google using the current location.

Now that you’ve got the header file set up, it’s time to add the implementation.

Implementing PlacesLoader

Open PlacesLoader.m and add these lines directly after #import "PlacesLoader.h":

//1
#import <CoreLocation/CoreLocation.h>
#import <Foundation/NSJSONSerialization.h>
 
//2
NSString * const apiURL = @"https://maps.googleapis.com/maps/api/place/";
NSString * const apiKey = @"...";
 
//3
@interface PlacesLoader ()
 
@property (nonatomic, strong) SuccessHandler successHandler;
@property (nonatomic, strong) ErrorHandler errorHandler;
@property (nonatomic, strong) NSMutableData *responseData;
 
@end
  1. First you import CoreLocation.h so you can work with the CLLocation class. The next import is for handling the JSON-response from the Places API.
  2. Next you define two NSString constants. The first is the URL of the Places API. IMPORTANT: Replace “…” with the API key you saved to your disk.
  3. Here comes another class extension! I like them because they help keep public and private stuff separate even if Objective-C doesn’t allow for private variables and methods. In this extension, you add a property for the success and errorHandler blocks. You also add an NSMutableData property to store the chunks of the response.

Next add the sharedInstance method directly after the @implementation (still in PlacesLoader.m):

+ (PlacesLoader *)sharedInstance {
	//1
	static PlacesLoader *instance = nil;
	static dispatch_once_t onceToken;
 
	//2
	dispatch_once(&onceToken, ^{
		instance = [[PlacesLoader alloc] init];
	});
 
	//3
	return instance;
}
  1. This is a simple method that declares a static variable for the instance and a dispatch_once_t token.
  2. With this token, you can use the dispatch_once macro to allocate the PlacesLoader instance using GCD. The token makes sure that the dispatch_once is executed only once.
  3. After that, you return the instance.

Nice, you’re making good progress and are almost to your next build and run. But first, you should load the POIs so that you can see a bit more onscreen.

Add this new method to your the @implementation in PlacesLoader.m to bring it to life:

- (void)loadPOIsForLocation:(CLLocation *)location radius:(int)radius successHandler:(SuccessHandler)handler errorHandler:(ErrorHandler)errorHandler {
	//1
	_responseData = nil;
	[self setSuccessHandler:handler];
	[self setErrorHandler:errorHandler];
 
	//2
	CLLocationDegrees latitude = [location coordinate].latitude;
	CLLocationDegrees longitude = [location coordinate].longitude;
 
	//3
	NSMutableString *uri = [NSMutableString stringWithString:apiURL];
	[uri appendFormat:@"nearbysearch/json?location=%f,%f&radius=%d&sensor=true&types=establishment&key=%@", latitude, longitude, radius, apiKey];
 
	//4
	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[uri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0f];
 
	//5
	[request setHTTPShouldHandleCookies:YES];
	[request setHTTPMethod:@"GET"];
 
	//6
	NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
 
	//7
	[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
	NSLog(@"Starting connection: %@ for request: %@", connection, request);
}
  1. The first part makes sure that there is no old response data from a first connection, setting _responseData to nil and saving the success and error handler blocks.
  2. The second part gets the latitude and longitude from the CLLocation. Latitude and longitude are the coordinates for a point on the planet. They are like the x- and y-coordinates of a Cartesian coordinate system.
  3. With these coordinates, you can create a mutable string with the API URL and append the API call you need. Let’s have a closer look at this API call. The call has two path parameters:
    • nearbysearch is the service you call. It takes a location and returns all POIs that are within a given radius around this location.
    • json tells the API that you want the response in JSON format. You can also pass xml to get XML output.

    Next there are five GET parameters:

    • location is the location for which you want the POIs, based on the latitude and longitude you pass, separated by a comma.
    • radius means the radius for which you want the POIs around the location, passed in meters.
    • sensor must be true if you have the location from a location sensor like a GPS sensor. That means it must be true for mobile devices.
    • types is where you can pass a pipe (|) separated list of types you want for the POIs. You can find list of supported types Here.
    • key is the API key you must pass with every request.
  4. With this request string, you can now create an NSURLRequest. You create a NSURL with the string and pass it as the first parameter to requestWithURL:cachePolicy:timeoutInterval:. The second parameter tells the connection that it should load the data from the server each time, even if there is some data cached from a previous request. The last parameter sets the timeout for the request to 20 seconds.
  5. Now that you have your request, you do some configuration, like setting the HTTPMethod to GET.
  6. Here you create an NSURLConnection with this request and the PlacesLoader instance as its delegate.
  7. Finally you set the network activity indicator to visible and log the request to the console.

You should always show the network activity indicator when you do networking, so that the user knows your app is loading something from the Internet.

In order to receive data, you need to add three delegate methods to the PlacesLoader. Start by adding this method:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
	if(!_responseData) {
		_responseData = [NSMutableData dataWithData:data];
	} else {
		[_responseData appendData:data];
	}
}

This method is very simple. First it checks if the _responseData is nil, creates a new NSMutableData instance and initializes it with the received data, if needed. Otherwise it just appends the received data.

Add the second method right after:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
	[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
 
	id object = [NSJSONSerialization JSONObjectWithData:_responseData options:NSJSONReadingAllowFragments error:nil];
 
	if(_successHandler) {
		_successHandler(object);
	}
}

The networking is finished, so you can hide the activity indicator and can start parsing the JSON response. Since iOS 5, you can parse JSON by simply using the NSJONSerialization class.

The method - (id)JSONObjectWithData:options:error: takes an NSData object that contains the JSON string. In your case, this is the responseData. The passed option allows the JSON to start with objects that are not arrays or dictionaries.

The last parameter is an error object that you can use to receive a description when the JSON could not be parsed. To keep it simple, you don’t use it now and simply call the successHandler with the deserialized data.

Finally, add the third method:

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
	if(_errorHandler) {
		_errorHandler(error);
	}
 
	[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}

This method is called when the connection fails, so in this case you invoke the errorHandler, passing it the error object.

This is a great time for a new test, but before you build and run, open MainViewController.m and import PlacesLoader.h:

#import "PlacesLoader.h"

Then find locationManager:didUpdateLocations:. Inside the if statement, add the following code, right after the “more code” comment:

[[PlacesLoader sharedInstance] loadPOIsForLocation:[locations lastObject] radius:1000 successHandler:^(NSDictionary *response) {
	NSLog(@"Response: %@", response);
	//more code here
 
	} errorHandler:^(NSError *error) {
		NSLog(@"Error: %@", error);
}];

This starts loading a list of POIs that are within a radius of 1000 meters of the user’s current position, and prints them to the console.

Build and run, and watch the console’s output. It should look like this, but with other POIs:

Response: {
	"debug_info" =     (
	);
	"html_attributions" =     (
		"Eintr\U00e4ge aus <a href=\"http://www.gelbeseiten.de/\">GelbeSeiten\U00aeVerlagen</a>"
	);
	"next_page_token" = "CmRWAAAANwNrRg5nBRa6jsBxnZgEaIqt16IQ4VCx2jVN2gykJDYznUz7MNMfJZAtSllzPVYYbD1p1PQ33n0KZ_iTePpaLgUp-ru__dibulcsdo9iDRbLrDl2VRxJ2t10amGQtyRgEhA5QpuiFOxtZxc0yOUSaJj-GhT3hemgLr0Xoszq_ML3LA31GtayEw";
	results =     (
	{
	geometry =             {
	location =                 {
	lat = "50.514008";
	lng = "8.387684";
	};
	};
	icon = "http://maps.gstatic.com/mapfiles/place_api/icons/museum-71.png";
	id = 52b9465e06a42601fe1b8b48324d7a7572ad1300;
	name = "Schloss Braunfels";
	photos =             (
	{
	height = 485;
	"html_attributions" =                     (
	"Von einem Google-Nutzer"
	);
	"photo_reference" = "CoQBeAAAAL3CQ4wBaGNB8CQZNOGdqgBD0fP-TV0Mk1eKDRePYpxT_KM4tmOZryHOs2EivtlX5aD7sitZBiLc0nFEHl_9nZ4-6ORw2_Ex2jc5pGngtltHKCHETjGaFKSRi5uhTwxPaZlUaZvhl5qQxPn8wHPUcocJzrVDObN4X1CuEyyQ4qlYEhDM0oMZlc0SJ_ZHZXLw2PvEGhRSJdfUAG9c8zoHqAmjwOIecW9zGQ";
	width = 688;
	}
	);
	rating = 4;
	reference = "CnRvAAAAfwSZatxRSutp6hN3ayMZkwiWuQAfkSeXHgsDW35mzWa89jqGXEu830AbGCmGpjv0maLjaVtNBA25UG-GpprEpPUYv8hRkwXa0-3Al7xhxpyQMgkW75nN4atpcU718DqAyCfWRaanBryI-YqI0BrncxIQkt4EEEQ0UYIEp-g6IomqehoUl5qQSdavqgX51a6PVP_fcBxtMHQ";
	types =             (
	museum,
	establishment
	);
	vicinity = "Belzgasse 1, Braunfels";
	},
	{
	geometry =             {
	location =                 {
	lat = "50.51647";
	lng = "8.383039999999999";
	};
	};
	icon = "http://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png";
	id = 7e2d9ec40900e5309c13f869766a7df1341342b7;
	name = "Hotel Brauhaus Oberm\U00fchle";
	"opening_hours" =             {
	"open_now" = 1;
	};
	rating = "3.5";
	reference = "CoQBdgAAANTVnNYlB9uVn7r6MoM6yIQBepcJoYrX9siR7FZ7uMwxxEdXu4lAB1p9RMsKE7Lr7RY4CcBEbKtG3RnpNST_UZocboFmXMmPMaQ4292uTGYvVxTllznM9lt6X7qIgpBCducvux-MH3BwNFgD6LgcZgj0U-UGarKy6bJtgrviRF_kEhBWpiKa0ksoZjoM6FfGVLKHGhQDgaGS5Zl4e-6q-OuC3raONu-2HA";
	types =             (
	restaurant,
	lodging,
	food,
	establishment
	);
	vicinity = "Gebr\U00fcder-Wahl-Stra\U00dfe 19, Braunfels";
	},
	{
	geometry =             {
	location =                 {
	lat = "50.525986";
	lng = "8.38242";
	};
	};
	icon = "http://maps.gstatic.com/mapfiles/place_api/icons/cafe-71.png";
	id = 253a1b4729beb4b97b47d68e7c906d6adcbe0b82;
	name = "Orthop\U00e4dische Klinik Braunfels";
	reference = "CoQBfAAAAD6EHnaMQ2JbrbAIn5ul9Wt4POhtkBfL0mhdL3myH1qdRiI5BD37VZtOAJxz0Oe76sqm5i89ymtKdXhXIq8SBgS3meabHMvpa6qZXwSzB0u5N_NmctylzSYbg3jS-F-qkXo8_0KeMn58QoydFxT9P0Etu-rWTIQ06QIvSYFeTb2YEhCus-h3vgG-GGvjnIQ6ysRbGhS2bnhw0ySDb_Hu_bMscf1aCyX4Bw";
	types =             (
	hospital,
	cafe,
	food,
	health,
	establishment
	);
	vicinity = "Hasselbornring 5, Braunfels";
	},

Pardon my french! :]

Note that if you get NULL back for a response, try increasing the radius to a larger value. For example, Ray lives in the boonies and needed to increase the value to 5000 to start receiving a response! :]

The Places Class

The response from Google Places contains an array with a dictionary for each POI. For this app you will only use a subset of the provided information. To make the work easier, you’ll create a class to store the POI information.

Let’s start. Go to File\New\New File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class Place, make it a subclass of NSObject, click Next and then Create.

When you’re done, open Place.h and add this before the @interface:

@class CLLocation;

Also add the following properties:

@property (nonatomic, strong) CLLocation *location;
@property (nonatomic, copy) NSString *reference;
@property (nonatomic, copy) NSString *placeName;
@property (nonatomic, copy) NSString *address;

These properties will store the following:

  • location is the CLLocation for this place. You need it later to calculate the distance from the user’s current position.
  • You’ll need reference to get more detailed information for this place.
  • placeName is a more readable name for this place. Or did you know that “CnRpAAAAm-rRZvs1uYt6xG6qQ5-0fAsv08nek5WyxX2UryCjWlPIcTh_6rgraNYXHBtqKdwmLbbFiOER13mIxb3H-b66XdpTzGY70OKaGI5V-XoZfSVenJpplgcIWAMcwqrN8dz70r42rJm8ZzrowidI1tHuqxIQmZ2k_YQ6Cq33QkNwWxWEQxoURNU4l_F_BCtK9WO8cF8a01SarhY” means the next Starbucks? :]
  • address is – you guessed it – the address of the location.

It would be nice if there were an init method that took all of these parameters so that you don’t need to call [myPlace set...] for each property. Add one directly after the property declarations:

- (id)initWithLocation:(CLLocation *)location reference:(NSString *)reference name:(NSString *)name address:(NSString *)address;

Now open Place.m and implement the initializer method:

- (id)initWithLocation:(CLLocation *)location reference:(NSString *)reference name:(NSString *)name address:(NSString *)address {
	if((self = [super init])) {
		_location = location;
		_reference = reference;
		_placeName = name;
		_address = address;
	}
	return self;
}

I think this needs no further explanation. You assign the values to the corresponding instance variables and that’s it.

So far, your app can determine a user’s position and load a list of POIs inside the local area. You have a class that can store a place from this list, even if you don’t use it at the moment. What’s really missing is the ability to show the POIs on the map!

Displaying Places of Interest

To make an annotation on the mapView, you need yet another class. So once again go to File\New\New File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class PlaceAnnotation, make it a subclass of NSObject, click Next and then Create.

Open PlaceAnnotation.h replace the contents with the following:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
 
@class Place;
 
@interface PlaceAnnotation : NSObject <MKAnnotation>
 
- (id)initWithPlace:(Place *)place;
- (CLLocationCoordinate2D)coordinate;
- (NSString *)title;
 
@end

Here you’ve made the class implement the MKAnnotation protocol and defined three methods.

Next open PlaceAnnotation.m and replace the contents with the following:

#import "PlaceAnnotation.h"
#import "Place.h"
 
@interface PlaceAnnotation ()
@property (nonatomic, strong) Place *place;
@end
 
@implementation PlaceAnnotation
 
- (id)initWithPlace:(Place *)place {
	if((self = [super init])) {
		_place = place;
	}
	return self;
}
 
- (CLLocationCoordinate2D)coordinate {
	return [_place location].coordinate;
}
 
- (NSString *)title {
	return [_place placeName];
}
 
@end

This is fairly straightforward – the annotation just returns the coordinate and place name from the Place class you created earlier.

Now you have everything you need to show some POIs on the map!

Let’s go back to MainViewController.m and complete the locationManager:didUpdateLocations: method. Find the last “more code” comment that’s inside the successHandler block. Change that block to this:

[[PlacesLoader sharedInstance] loadPOIsForLocation:[locations lastObject] radius:1000 successHandler:^(NSDictionary *response) {
	NSLog(@"Response: %@", response);
	//1
	if([[response objectForKey:@"status"] isEqualToString:@"OK"]) {
		//2
		id places = [response objectForKey:@"results"];
		//3
		NSMutableArray *temp = [NSMutableArray array];
 
		//4
		if([places isKindOfClass:[NSArray class]]) {
			for(NSDictionary *resultsDict in places) {
				//5
				CLLocation *location = [[CLLocation alloc] initWithLatitude:[[resultsDict valueForKeyPath:kLatitudeKeypath] floatValue] longitude:[[resultsDict valueForKeyPath:kLongitudeKeypath] floatValue]];
 
				//6
				Place *currentPlace = [[Place alloc] initWithLocation:location reference:[resultsDict objectForKey:kReferenceKey] name:[resultsDict objectForKey:kNameKey] address:[resultsDict objectForKey:kAddressKey]];
 
				[temp addObject:currentPlace];
 
				//7
				PlaceAnnotation *annotation = [[PlaceAnnotation alloc] initWithPlace:currentPlace];
				[_mapView addAnnotation:annotation];
			}
		}
 
		//8
		_locations = [temp copy];
 
		NSLog(@"Locations: %@", _locations);
	}
} errorHandler:^(NSError *error) {
	NSLog(@"Error: %@", error);
}];

Let’s have a closer look at what’s happening above:

  1. The first if statement is a simple check that everything’s fine. If the status is not OK, something went wrong and you don’t need to waste your time trying to do something useful with the response.
  2. This takes the list of POIs from the response. The list can be an NSArray or an NSDictionary when you live in a boring area where only one POI is nearby. :]
  3. Here you create a temporary mutable array to store the places.
  4. In this example, you only work with a list of POIs and not with a single POI, so this checks if places is an NSArray and if so, it iterates over the entries. I leave it as an exercise for you to add support for a response with a single point. (Hint: In that case, places will be an NSDictionary.)
  5. The next line creates a CLLocation with the latitude and longitude of this place.
  6. With this location you can initialize a Place instance and add it to the temp array.
  7. The place is used to create a PlaceAnnotation and add it to the map.
  8. After all places have been processed, you set _locations to a copy of temp and log it to the console.

As you’ve seen, there is a lot of red-marked code at the moment. To make the compiler happy, you need to make some changes on top of your .m file. Scroll to the top of the file and add these lines:

#import "Place.h"
#import "PlaceAnnotation.h"
 
NSString * const kNameKey = @"name";
NSString * const kReferenceKey = @"reference";
NSString * const kAddressKey = @"vicinity";
NSString * const kLatitudeKeypath = @"geometry.location.lat";
NSString * const kLongitudeKeypath = @"geometry.location.lng";

Also add the following to the class extension:

@property (nonatomic, strong) NSArray *locations;

Build and run. This time, some annotations appear on the map and when you tap one, you’ll see the name of the place. This app looks nice for now, but where is the augmented reality?!

MapViewAnnotations

Introducing the AR ToolKit

You’ve done a lot of work so far, but they’ve been necessary preparations for what you’re about to do: it’s time to bring augmented reality to the FlipSideViewController.

This controller will show the camera view with overlays for the different POIs. You’ll see the name and the distance of the POIs from the current location, and when you tap one, you’ll see some details about this POI. To achieve all of this, you’ll use the iPhone AR Toolkit, which you can download here.

You may ask how this toolkit can make your life easier. There are several ways.

First, like the String SDK, AR Toolkit handles the camera captioning for you so that showing live video is easy. Second, it adds the overlays for the POIs for you and handles their positioning.

As you’ll see in a moment, the last point is perhaps your greatest boon, because it saves you from having to do some complicated math! If you want to know more about the math behind AR Toolkit, continue on.

If, on the other hand, you want to dig immediately into the code, feel free to skip the next two sections and jump straight to Let’s Start Coding.

Warning, Math Inside!

So you want to learn more about the math behind the AR Toolkit. That’s great! Be warned, however, that it’s a bit more complicated than standard arithmetic. In the following examples, we assume that there are two given points, A and B, that hold the coordinates of a specific point on the earth.

A point’s coordinates consist of two values: longitude and latitude. These are the geographic names for the x- and y-values of a point in the 2D Cartesian system.

  • Longitude specifies if a point is east or west of the reference point in Greenwich, England. The value can be from +180° to -180°.
  • Latitude specifies if a point is north or south of the equator. The range is from 90° at the north pole to -90° at the south pole.

If you have a look at a standard globe, you’ll see lines of longitude that go from pole to pole – these are also known as meridians. You’ll also see lines of latitude that go around the globe that are also called parallels. You can read in geography books that the distance between two parallels is around 111 km, and the distance between two meridians is also around 111km.

With this in mind, you can calculate the distance between two points on the globe with these formulas:

Formel1

Formel2

This gives you the distances for latitude and longitude, which are two sides of a right triangle. Using the Pythagorean theorem, you can now calculate the hypotenuse of the triangle to find the distance between the two points:

Formel3

That’s quite easy but unfortunately, it’s also wrong.

If you look again at your globe, you’ll see that the distance between the parallels is almost equal, but the meridians meet at the poles. So the distance between meridians shrinks when you come closer to the poles, and is zero on the poles. This means the formula above works only for points near the equator. The closer the points are to the poles, the bigger the error becomes.

To calculate the distance more precisely, you can determine the great-circle distance. This is the distance between two points on a sphere and, as we all know, the earth is a sphere. Well OK, it is nearly a sphere, but this method gives you good results. With a known latitude and longitude for two points, you can use the following formula to calculate the great-circle distance.

Distanz_Kugel

This formula gives you the distance between two points with an accuracy of around 60 km, which is quite good if you want to know how far Tokyo is from New York. For points closer together, the result will be much better.

Phew – that was hard stuff! The good news is that CLLocation has a method, distanceFromLocation:, that does this calculation for you. AR Toolkit also uses this method.

Why AR Toolkit

You may be thinking to yourself “Meh, I still don’t see why I should use AR toolkit.” It’s true, grabbing frames and showing them is not that hard and you can read about it on this site. You can calculate the distance between points with a method from CLLocation without bleeding. And how to show an overlay on a video is explained here.

So why did I introduce this toolkit? The problem comes when you need to calculate where to show the overlay for a POI on the screen. Lets assume you have a POI that is to the north of you and your device is pointing to the northeast. Where should you show the POI – centered or to the left side? At the top or bottom?

It all depends on the current position of the device in the room. If the device is pointing a little towards the ground, you must show the POI nearer to the top. If it’s pointing to the south, you should not show the POI at all. This could quickly get complicated!

And that’s where the AR Toolkit is most useful. It grabs all the information needed from the gyroscope and compass and calculates where the device is pointing and its degree of tilt. Using this knowledge, it decides if and where a POI should be displayed on the screen.

Plus, without needing to worry about showing live video and doing complicated and error-prone math, you can concentrate on writing a great app your users will enjoy using.

Let’s Start Coding

Download the AR Toolkit if you haven’t already. Open the iPhone-AR-Toolkit folder and drag the ARKit folder into Xcode’s project navigator. Make sure that Copy items into destination group’s folder and Create groups for any added folders are selected.

Add_ArKit

Now have a quick look at the files you added:

ArKit_Files

Let’s go over these briefly:

  • ARCoordinate: This class is used to track coordinates and their titles. The AugmentedRealityController uses this.
  • ARGeoCoordiante: This is a wrapper class for CLLocation that has also a displayView property, which is shown from the AugmentedRealityController as a marker for this coordinate.
  • ARKit: This is the main class and the only header you must import to start working with the AR Toolkit.
  • ARLocationDelegate: The delegate is used to get a list of ARGeoCoordinates.
  • ARViewProtocol: This header declares two protocols. ARMarkerDelegate is used to tell the delegate when the user tapped on a marker and ARDelegate is similar to the LocationManagerDelegate protocol.
  • AugmentedRealityController: This controller does all the visual things for you. It shows a live video and adds markers to the view.
  • GEOLocations: is not used in this tutorial, so you can ignore it for now.

Setting Up the AR View

Open FlipsideViewController.h and add the following after you import UI Kit:

#import <MapKit/MapKit.h>
#import "ARKit.h"

Next you need to let your controller implement some delegate protocols. Change the @interface section of the same file so that it looks like this:

 
@interface FlipsideViewController : UIViewController <ARLocationDelegate, ARDelegate, ARMarkerDelegate>

Now you need to add two strong properties, one to hold all the POIs you got from Google and another to store the current location of the user. Add them after the delegate:

 
@property (nonatomic, strong) NSArray *locations;
@property (nonatomic, strong) MKUserLocation *userLocation;

Now open FlipsideViewController.m. Add a class extension and add two properties to it:

@interface FlipsideViewController () 
 
@property (nonatomic, strong) AugmentedRealityController *arController;
@property (nonatomic, strong) NSMutableArray *geoLocations;
 
@end

arController holds a reference to your AugmentedRealityController and geoLocations is an array that stores ARGeoCoordinates.

Now create an instance of AugmentedRealityController. To do this, replace viewDidLoad with the following:

- (void)viewDidLoad
{
	[super viewDidLoad];
 
    if(!_arController) {
        _arController = [[AugmentedRealityController alloc] initWithView:[self view] parentViewController:self withDelgate:self];
    }
 
    [_arController setMinimumScaleFactor:0.5];
    [_arController setScaleViewsBasedOnDistance:YES];
    [_arController setRotateViewsBasedOnPerspective:YES];
    [_arController setDebugMode:NO];
}

The method starts with a lazy initialization of the AugmentedRealityController. The initializer takes three parameters.

  • The first parameter is a UIView where the ARController should show the video and markers.
  • You’ll use the second parameter, parentViewController, to handle device orientations.
  • The last parameter delegate is your view controller that implements the ARDelegate protocol.

The next lines set some properties that handle how you show the markers:

  • mimimumScaleFactor: This is the minimal size for marker views, used in case you set the next property to YES.
  • scaleViewBasedOnDistance: If this is YES, the marker views will be scaled according to their distance from the user location. That means locations that are far away will appear smaller than locations that are closer.
  • rotateViewsBasedOnPerspective: This defines if marker views should rotate with the device.
  • debugMode: If YES, some additional views will be shown on the screen.

Implementing the Delegate Methods

Now have a look at the ARDelegate protocol. As mentioned before, this protocol is similar to the CLLocationManagerDelegate protocol. It has some methods that inform the delegate about location-related updates.

All three methods in this protocol are required, so you must implement them. In this tutorial, you’re not interested in orientation, heading or location changes, so just these empty methods (still in FlipsideViewController.m):

-(void)didUpdateHeading:(CLHeading *)newHeading {
 
}
 
-(void)didUpdateLocation:(CLLocation *)newLocation {
 
}
 
-(void)didUpdateOrientation:(UIDeviceOrientation)orientation {
 
}

While you’re at it, also add some stub implementations for the ARLocationDelegateARMarkerDelegate delegate methods:

- (void)didTapMarker:(ARGeoCoordinate *)coordinate {
}
 
- (NSMutableArray *)geoLocations {
    return nil;
}

Build and run, and tap the camera button on the map view to go to the flipside view controller. You should now see video output from your camera:

ARKit_VideoScreen

Displaying the Locations

The next step is to convert the Places to ARGeoCoordinates that can be shown onscreen.

Start by importing your Place class at the top of FlipsideViewController.m:

#import "Place.h"
</pre
 
Next add a method named <code>- (void)generateGeoLocations</code>:
 
<pre lang="objc">
- (void)generateGeoLocations {
	//1
	[self setGeoLocations:[NSMutableArray arrayWithCapacity:[_locations count]]];
 
	//2
	for(Place *place in _locations) {
		//3
		ARGeoCoordinate *coordinate = [ARGeoCoordinate coordinateWithLocation:[place location] locationTitle:[place placeName]];
		//4
		[coordinate calibrateUsingOrigin:[_userLocation location]];
 
		//more code later 
 
		//5
		[_arController addCoordinate:coordinate];
		[_geoLocations addObject:coordinate];
	}
}

Let’s review this section by section:

  1. The first line allocates the _geoLocations array where you will store all ARGeoCoordinate.
  2. After that, you iterate over all places in the _locations array.
  3. For each place you create an ARGeoCoordinate instance. A coordinate needs a CLLocation and a name. Both are taken from the place.
  4. After that, the coordinate needs to be calibrated, and you use the user’s current location for this. During calibration, you calculate the distance between the two locations.
  5. At the end you add the coordinate to your _arController so that it can be shown on the screen. You also add it to _geoLocations.

In this method you use the _locations array but at the moment this array is not set up, which you need to change. You have an array with all places in your MainViewController, so there is no need to load them again from the web.

Open MainViewController.m, find the method - (void)prepareForSegue:sender: and add the following two lines right after the line [[segue destinationViewController] setDelegate:self];:

[[segue destinationViewController] setLocations:_locations];
[[segue destinationViewController] setUserLocation:[_mapView userLocation]];

You have all places and the current location in your FlipsideViewController and can go back to FlipsideViewController.m. An ARGeoCoordinate needs a view that can be shown on the screen. Before you create such a view, go back to the generateGeoLocation method and add the next two lines inside the for loop. Add them directly after the //more code later comment:

MarkerView *markerView = [[MarkerView alloc] initWithCoordinate:coordinate delegate:self];
[coordinate setDisplayView:markerView];

Here you create a MarkerView for the coordinate and set the displayView of the coordinate to this view. Now your project won’t compile anymore, which you need to fix.

Create a new Objective-C class called MarkerView and make it a subclass of UIView. After saving it, go to MarkerView.h and replace its content with this:

#import <UIKit/UIKit.h>
 
//1
@class ARGeoCoordinate;
@protocol MarkerViewDelegate;
 
@interface MarkerView : UIView
 
//2
@property (nonatomic, strong) ARGeoCoordinate *coordinate;
@property (nonatomic, weak) id <MarkerViewDelegate> delegate;
 
//3
- (id)initWithCoordinate:(ARGeoCoordinate *)coordinate delegate:(id<MarkerViewDelegate>)delegate;
 
@end
 
//4
@protocol MarkerViewDelegate <NSObject>
 
- (void)didTouchMarkerView:(MarkerView *)markerView;
 
@end
  1. The first part is a forward declaration for ARGeoCoordinate and a delegate protocol.
  2. You need both to declare the properties.
  3. You also need both for the init method. You will use the coordinate to show the distance of the POI represented by the marker. init takes the coordinate and delegate as parameters.
  4. This is the MarkerViewDelegate protocol. As you can see, you only need one method in this protocol, and you use it to inform the delegate when the view was touched.

Open MarkerView.m and import ARGeoCoordinate.h. Now add two constants after the import:

#import "ARGeoCoordinate.h"
 
const float kWidth = 200.0f;
const float kHeight = 100.0f;

This is the size of your MarkerView. You use these constants to make sure that you can’t forget places in your code when you want to change the size. The MarkerView will have a title label that will never change and a label to display the distance.

The distance will change when your users move and you need a reference to it so you can change the text. Add this reference as a property in a class extension:

@interface MarkerView ()
 
@property (nonatomic, strong) UILabel *lblDistance;
 
@end

initWithCoordinate:delegate: is straightforward. Add it:

- (id)initWithCoordinate:(ARGeoCoordinate *)coordinate delegate:(id<MarkerViewDelegate>)delegate {
	//1
	if((self = [super initWithFrame:CGRectMake(0.0f, 0.0f, kWidth, kHeight)])) {
 
		//2
		_coordinate = coordinate;
		_delegate = delegate;
 
		[self setUserInteractionEnabled:YES];
 
		//3
		UILabel *title = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, kWidth, 40.0f)];
		[title setBackgroundColor:[UIColor colorWithWhite:0.3f alpha:0.7f]];
		[title setTextColor:[UIColor whiteColor]];
		[title setTextAlignment:NSTextAlignmentCenter];
		[title setText:[coordinate title]];
		[title sizeToFit];
 
		//4		
		_lblDistance = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 45.0f, kWidth, 40.0f)];		
		[_lblDistance setBackgroundColor:[UIColor colorWithWhite:0.3f alpha:0.7f]];
		[_lblDistance setTextColor:[UIColor whiteColor]];
		[_lblDistance setTextAlignment:NSTextAlignmentCenter];
		[_lblDistance setText:[NSString stringWithFormat:@"%.2f km", [coordinate distanceFromOrigin] / 1000.0f]];
		[_lblDistance sizeToFit];
 
		//5
		[self addSubview:title];
		[self addSubview:_lblDistance];
 
		[self setBackgroundColor:[UIColor clearColor]];
	}
 
	return self;
}
  1. First you initialize the label with the defined width and height.
  2. In the next few lines, you set the properties and enable user interaction. This means that your view will receive messages about touches on it.
  3. This code block initializes and configures the title label. Because the text will never change, you don’t need a property for this label.
  4. Here you do the same for the distance label. You set the text, calling the method distanceFromOrigin of the ARGeoCoordinate. This is a property that holds the distance of this coordinate from the point that was set as the origin – in this case, the position of the iDevice.
  5. At the end, you add the labels to the view and set the background color of the view itself to clearColor.

Every time the program draws the label, you’ll update the text of _lblDistance. To do this you must override drawRect:, the method called when the view needs to be redrawn, which is every time the view appears or when it’s been partially hidden by another view.

To do this, add the following method of the file:

- (void)drawRect:(CGRect)rect {
	[super drawRect:rect];
	[[self lblDistance] setText:[NSString stringWithFormat:@"%.2f km", [[self coordinate] distanceFromOrigin] / 1000.0f]];
}

Before you build and run, go back to FlipSideViewController.m and import MarkerView.h:

#import "MarkerView.h"

Also replace the geoLocations method with the following:

- (NSMutableArray *)geoLocations {
	if(!_geoLocations) {
		[self generateGeoLocations];
	}
	return _geoLocations;
}

And call it in viewWillAppear::

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self geoLocations];
}

Now give in to the urge to build and run. You’ll see something like this, of course pertaining to your own specific location:

IMG_1007

Sweet you’ve done it – your own location based augmented reality app!

Finishing Touches

For a finishing touch, let’s make the app display an “additional info” view when the user taps on a marker view.

To do this, you’ll need to handle touches in your marker view and tell the delegate that a view was touched. Luckily, this part is pretty simple. Add the following method to MarkerView.m:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
	if(_delegate && [_delegate conformsToProtocol:@protocol(MarkerViewDelegate)]) {
		[_delegate didTouchMarkerView:self];
	}
}

You call touchesEnded when the user’s finger leaves the screen and all you have to do is to tell the delegate that your view was touched. You pass self as a parameter so that the delegate knows which view was touched.

The if statement is just to make sure that the delegate is set and that it implements the MarkerViewDelegate protocol, meaning that it implements all needed methods in this protocol. Because MarkerViewDelegate has only one method, you can also use [_delegate respondsToSelector:@selector(didTouchMarkerView:)];, which will return YES when the delegate has a method with this name and parameters.

Note: You can also use a method called -respondsToSelector: to check if a delegate can call a specific selector. This can be helpful if your protocol has optional methods. If you don’t make such a check before calling an unimplemented selector, your app will crash!

You need one last method in your MarkerView. If a view receives touch events, they will be forwarded to all subviews that have userInteractionEnabled set to YES, like your MarkerViews. This happens even when the touch was in the upper left corner and the subview is in the lower right corner. So you have to check to see whether the touch was inside your MarkerView.

Every time a view is touched it calls hitTest:WithEvent: on all its subviews that are visible and have userInteracitonEnabled set to YES. hitTest:WithEvent: calls pointInside:withEvent: to check if a touch was inside a view. If pointInside:withEvent: returns YES, all subviews of this subview will receive hitTest:withEvent: and so on. This is done until all subviews have been traversed and the view that was touched has been found.

To find the tapped MarkerView, you have to check if a point is inside this MarkerView by overriding pointInside:withEvent: in MarkerView.m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
	CGRect theFrame = CGRectMake(0, 0, kWidth, kHeight);
 
	return CGRectContainsPoint(theFrame, point); 
}

The MarkerViews are scaled dependent on the distance from the user’s position, but you want to do the hitTest as though they are their original size so that it’s easier to touch a marker. To achieve this, you create a CGRect with the unscaled size and call CGRectContainsPoint(CGRect, CGPoint), which returns true if the point is inside the given rect.

It’s time to finish the app. You’ll perform the next steps inside FlipsideViewController.m, so open the file. As you are at the top of the file at the moment, add some new constants and let FlipsideViewController implement the MarkerViewDelegate protocol:

#import "MarkerView.h"
 
NSString * const kPhoneKey = @"formatted_phone_number";
NSString * const kWebsiteKey = @"website";
 
const int kInfoViewTag = 1001;
 
@interface FlipsideViewController () <MarkerViewDelegate>

The two NSString constants are the keys for the response of another Places API request you will send, and the integer is the tag of the infoview you’ll show when the user taps a marker. Every time the user taps a marker, you send a request to the Places API to load additional information for this POI. The PlacesLoader needs a new method for this request.

Open PlacesLoader.h and add the following method:

- (void)loadDetailInformation:(Place *)location successHanlder:(SuccessHandler)handler errorHandler:(ErrorHandler)errorHandler;

Also add a forward declaration after your imports, just below declaring class CLLocation:

@class Place;

Next open PlacesLoader.m and add this import:

#import "Place.h"

Also implement the method that you declared in the header:

- (void)loadDetailInformation:(Place *)location successHanlder:(SuccessHandler)handler errorHandler:(ErrorHandler)errorHandler {
	_responseData = nil;
	_successHandler = handler;
	_errorHandler = errorHandler;
 
	NSMutableString *uri = [NSMutableString stringWithString:apiURL];
 
	[uri appendFormat:@"details/json?reference=%@&sensor=true&key=%@", [location reference], apiKey];
 
	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[uri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0f];
 
	[request setHTTPShouldHandleCookies:YES];
	[request setHTTPMethod:@"GET"];
 
	NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
 
	NSLog(@"Starting connection: %@ for request: %@", connection, request);
}

This method works like the previous one, but the API method that you call is another one called “details”. You have to pass the reference for the POI about which you want the details. Luckily, this reference is already stored in a Place instance and so you can access it through the reference property.

That’s all there is to do here. Go back to FlipsideViewController.m and add this import to the top of the file:

#import "PlacesLoader.h"

Also add the following new method:

- (void)didTouchMarkerView:(MarkerView *)markerView {
	//1
	ARGeoCoordinate *tappedCoordinate = [markerView coordinate];
	CLLocation *location = [tappedCoordinate geoLocation];
 
	//2
	int index = [_locations indexOfObjectPassingTest:^(id obj, NSUInteger index, BOOL *stop) {
		return [[obj location] isEqual:location];
	}];
 
	//3
	if(index != NSNotFound) {
		//4
		Place *tappedPlace = [_locations objectAtIndex:index];
		[[PlacesLoader sharedInstance] loadDetailInformation:tappedPlace successHanlder:^(NSDictionary *response) {
			//5
			NSLog(@"Response: %@", response);
			NSDictionary *resultDict = [response objectForKey:@"result"];
			[tappedPlace setPhoneNumber:[resultDict objectForKey:kPhoneKey]];
			[tappedPlace setWebsite:[resultDict objectForKey:kWebsiteKey]];
			[self showInfoViewForPlace:tappedPlace];
		} errorHandler:^(NSError *error) {
			NSLog(@"Error: %@", error);
		}];
	}
}

Let’s go over this step by step:

  1. First you get the CLLocation of the touched marker.
  2. With this location, you can search the corresponding Place in the _location array. You use - (NSUInteger)indexOfObjectPassingTest: to find the index of the Place. This method takes a block as its parameter. In this block you check if the location of a Place is equal to the location of the marker view. After that, you should have the index of the Place inside the _locations array.
  3. A little bit of error handling is always a good idea and so the if statement checks if the index is not NSNotFound. NSNotFound is an NSInteger with the value of NSIntegerMax and means that the object was not found in the array.
  4. Now that you have the index, you can get the Place from the array and store it in tappedPlace. The next line loads some details for this Place using the new method from PlacesLoader.
  5. After you receive the response, you call the success block and store the phone number and website in tappedPlace. Finally, you call showInfoViewForPlace:.

Add these new properties directly after the others in Place.h:

@property (nonatomic, copy) NSString *phoneNumber;
@property (nonatomic, copy) NSString *website;

Back in FlipsideViewController.m, add showInfoViewForPlace::

- (void)showInfoViewForPlace:(Place *)place {
	CGRect frame = [[self view] frame];
	UITextView *infoView = [[UITextView alloc] initWithFrame:CGRectMake(50.0f, 50.0f, frame.size.width - 100.0f, frame.size.height - 100.0f)];
	[infoView setCenter:[[self view] center]];
	[infoView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
 
	//1
	[infoView setText:[place infoText]];
	[infoView setTag:kInfoViewTag];
	[infoView setEditable:NO];
 
	[[self view] addSubview:infoView];
}

The method is not that complicated and simply creates a UITextView and adds it as a subview. Have a closer look at the three lines marked with //1. The text for the text field comes from the method infoText of the place. The next line gives the infoView a unique tag so that you can remove it later from the view, and the last line makes the view not editable, so it’s just like a label.

infoText still needs to be declared, so declare it in Place.h:

//...
- (id)initWithLocation:(CLLocation *)location reference:(NSString *)reference name:(NSString *)name address:(NSString *)address;
- (NSString *)infoText;

And implement it in Places.m:

- (NSString *)infoText {
	return [NSString stringWithFormat:@"Name:%@\nAddress:%@\nPhone:%@\nWeb:%@", _placeName, _address, _phoneNumber, _website];
}

This method returns an NSString that shows the name, address, phone number and website.

One last thing and your app is finished. At the moment, the info view remains on the screen and hides a great deal of the ARView. It’s a good idea to remove the info view when the user taps outside of it. This is easy to do.

You disabled userInteraction for the infoView, so the FlipsideViewControllers view only receives touch events when the user taps outside the info view. All you need to do is implement touchesEnded:withEvent: inside FlipsideViewControllers.m, like this:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
	UIView *infoView = [[self view] viewWithTag:kInfoViewTag];
 
	[infoView removeFromSuperview];	
}

You get the infoView from the flipsideViewControllers view using its tag and can remove it. Build and run one last time, and enjoy your location-based AR app!

Location based augmented reality app

Where to Go from Here?

Here is a source file with all of the code from the above project.

Congratulations, you now know how to make your own location based augmented reality app! And as a bonus, you’ve also gotten a short introduction to the Google Places API.

Want to learn more about augmented reality? Check out our marker tracking augmented reality iOS tutorial, where you’ll learn how make an app that makes a 3D image pop out when pointed at a marker.

In the meantime, if you have any comments or questions, please join the forum discussion below!

Jean-Pierre Distler
Jean-Pierre Distler

Jean-Pierre Distler is an iOS Developer in Braunfels, Germany. He develops apps for 3+ years and is also interested in IT-Security and penetration testing.

When he's not on his computer he enjoys time with his daughters.

User Comments

77 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 ]
  • Hi, first of all congratulations on such a good piece of tutorial, keep it up.

    So i followed all the steps and ended up with app fully functional, but i got proposed to add a few features to it. For example on the view that supports the infos about the local you "tap" to see i wanted to add something new. For example, would be great if i could enter the site that's on description, or maybe call the number.

    Any of you have any idea how could i implement this kind of features?

    Thanks again for the great tutorial.

    Cumps
    P.Correia
  • Hi,

    UITextView has a property named dataDetectorTypes. Setting this you can turn number and web address detection on. Tapping on it will automatically open the corresponding app like Safari.
    If you want to customise this behaviour the UITextViewDelegate method
    Code: Select all

    textView:shouldInteractWithURL:inRange:

    can help you.
    Jean-Pierre Distlerpierredrks
[ 1 , 2 , 3 , 4 , 5 , 6 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: Best iOS Animations in 2014. [Read Now]!

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Corinne Krych

... 49 total!

Update Team

  • Ray Fix

... 15 total!

Editorial Team

  • Alexis Gallagher

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

... 32 total!

Subject Matter Experts

... 4 total!