Creating a treasure hunt app using an iPad and an iPhone

In this tutorial I will show how to use iBeacon - Apple's indoor proximity system. We're going to build a treasure hunt where the iPad acts as the treasure and the iPhone acts as the hunter which tells you when you're getting warmer. If you want to following along with this tutorial the [is available to download here][Source].

Creating the project

We're going to use a single code base for both the treasure (transmitter) and the hunter (receiver). The code will determine whether it's running on an iPhone or iPad and configure itself accordingly. With that in mind, fire up XCode and create a universal single view application. New project dialog in XCode
New project dialog in XCode

Add required frameworks

Our hunter app is going to speak to us like a salty pirate (kind of), for this we need AVFoundation. For iBeacon we need CoreBluetooth and CoreLocation so go ahead and add all three. The image below shows all the linked frameworks in my project.

Linked frameworks frame in XCode

Prepare your views

For our treasure app (the iPad version) we just need a big image of treasure so I won't go into detail about adding this, this iPad view doesn't have any interactive content so manipulate the Main_iPad.storyboard so that your view will look like this:

iPad app example

The iPhone app (the hunter) needs to be a little more interactive. As you get closer to the treasure the background colour and a label will change to look like this:

iPhone app example

Again, I'll let you manipulate the view storyboard but essentially we need a single UILabel that's attached to an IBOutlet on the main view controller. Go ahead and add a UILabel to the Main_iPhone.storyboard file and bind it to the only view controller in the project. I've named my label statusLabel as you can see below: iPhone view creation Left iPhone view creation Right

Configure your view controller

Jump over to your viewcontroller header file and import AVFoundation, CoreBluetooth and CoreLocation:

#import <AVFoundation/AVFoundation.h>
#import <CoreBluetooth/CoreBluetooth.h>
#import <CoreLocation/CoreLocation.h>

Because the iPad (the treasure) needs to know when the bluetooth peripheral is ready before it starts advertising itself as a beacon; our viewcontroller needs to implement the CBPeripheralManagerDelegate, also since our iPhone (the hunter) needs to know when a beacon is near it needs to implement CLLocationManagerDelegate. Go ahead and decorate your controller with these now:

@interface THViewController : UIViewController<CBPeripheralManagerDelegate, CLLocationManagerDelegate>

Configuring the transmitter iBeacons transmit three values:

  • proximityUUID - a property which identifies the vendor. All beacons by a vendor should use the same ID and realistically the app should only be looking out for one vendor code.
  • major - a numeric property to specify a set of related beacons. This can be used to break down a region or to group beacons
  • minor - the minor number ultimately identifies the actual beacon. For example if you use the major value to identify a city or a particular store in a chain of stores then the minor number would identify the beacon in that store.

Since we've only got one beacon all we really need to care about is the UUID. To generate a UUID I used the uuidgen command line tool in terminal:

Terminal command line tool

Go ahead and do the same and then add two constants to your viewcontroller implementation...

static NSString * uuid = @"4234EE23-34A0-4BF7-993A-FE5574A70762";
static NSString * treasureId = @"com.eden.treasure";

Now in order to transmit we're going to need three things:

A CLBeaconRegion for advertising ourselves A CBPeripheralManager to tell us when the bluetooth device on our iPad is ready to use and An NSDictionary containing our preferences for broadcasting Add these as properties to the interface in the .m file:

@property CLBeaconRegion * beaconRegion;  
@property CBPeripheralManager * peripheralManager;  
@property NSMutableDictionary * peripheralData;

We're going to branch our logic in the viewDidLoad method of the controller to switch functionality based on whether or not the device is an iPad (treasure) or an iPhone (the hunter). Copy the following viewDidLoad method:

- (void)viewDidLoad  
{  
   [super viewDidLoad];  

   // Regardless of whether the device is a transmitter or receiver, we need a beacon region.  
   NSUUID * uid = [[NSUUID alloc] initWithUUIDString:uuid];  
   self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uid identifier:treasureId];  
   // When set to YES, the location manager sends beacon notifications when the user turns on the display and the device is already inside the region.  
   [self.beaconRegion setNotifyEntryStateOnDisplay:YES];  
   [self.beaconRegion setNotifyOnEntry:YES];  
   [self.beaconRegion setNotifyOnExit:YES];  
   if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {  
     [self configureTransmitter];  
   }  
   else {  
     [self configureReceiver];  
   }  
}

What we're doing here is setting up a region regardless of whether or not we're on iPhone or iPad since we'll use the same region in both and then we're configuring the region before branching our configuration code based what's reported by the user interface idiom.

The configureTransmitter code is relatively straightforward:

-(void)configureTransmitter {  
  // The received signal strength indicator (RSSI) value (measured in decibels) for the device. This value represents the  measured strength of the beacon from one meter away and is used during ranging. Specify nil to use the default value for the device.  
  NSNumber * power = [NSNumber numberWithInt:-63];  
  self.peripheralData = [self.beaconRegion peripheralDataWithMeasuredPower:power];  
  // Get the global dispatch queue.  
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
  // Create a peripheral manager.  
  self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:queue];  
}  

Here we set a power adjustment before getting the peripheral data from the beacon before setting ourselves as a delegate and then initializing the peripheral manager. We can't start broadcasting ourselves as a beacon until the bluetooth adapter on the iPad is turned on and ready and so we do this in the CBPeripheralManagerDelegate.

Go ahead and implement the only method we need from the CBPeripheralManagerDelegate now:

-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {  
  // The peripheral is now active, this means the bluetooth adapter is all good so we can start advertising.  
  if (peripheral.state == CBPeripheralManagerStatePoweredOn) {  
    [self.peripheralManager startAdvertising:self.peripheralData];  
  }  
  else if (peripheral.state == CBPeripheralManagerStatePoweredOff) {  
    NSLog(@"Powered Off");  
    [self.peripheralManager stopAdvertising];  
  }  
} 

All we're doing is waiting for the bluetooth device to be powered on before we start transmitting (or advertising) our region and that's all we need to do.

Configure the receiver

Now we need to configure the receiver but before we do we need a couple more properties. Add the following properties to the interface within the .m file:

@property CLLocationManager * locationManager;  
@property CLProximity previousProximity;  

Now add the configure receiver code:

-(void)configureReceiver {  
  // Location manager.  
  self.locationManager = [[CLLocationManager alloc] init];  
  self.locationManager.delegate = self;  
  [self.locationManager startMonitoringForRegion:self.beaconRegion];  
  [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];  
}  

All we're doing here is setting up a location manager and monitoring for the region we configured earlier. Now we need to configure what happens when we enter the region, exit a region or when we 'range beacons' which is the event which occurs when we get closer or further away from a beacon. Let's start with entering and exiting a region:

-(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {  
  // See if we've entered the region.  
  if ([region.identifier isEqualToString:treasureId]) {  
    UILocalNotification * notification = [[UILocalNotification alloc] init];  
    notification.alertBody = @"There be a treasure hiding nearby. Find it me hearties.";  
    notification.soundName = @"arrr.caf";  
    [[UIApplication sharedApplication] presentLocalNotificationNow:notification];  
  }  
}  
-(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {  
  // See if we've exited a treasure region.  
  if ([region.identifier isEqualToString:treasureId]) {  
    UILocalNotification * notification = [[UILocalNotification alloc] init];  
    notification.alertBody = @"Avast ye bilge rat. We be losing sight of the treasure.";  
    notification.soundName = @"arrr.caf";  
    [[UIApplication sharedApplication] presentLocalNotificationNow:notification];  
  }  
} 

Here we are presenting a local notification whenever we're entering or exiting a region with a salty pirate message. The clever stuff happens in the locationManager:didRangeBeacons:inRegion method of CLLocationManagerDelegate:

-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region  
{  
  if ([beacons count] == 0)  
    return;  
    NSString * message;  
    UIColor * bgColor;  
    CLBeacon * beacon = [beacons firstObject];  
    switch (beacon.proximity) {  
      case CLProximityUnknown:  
      message = @"Come 'ere me buxom beauty. There be no treasure in me vicinity";  
      bgColor = [UIColor blueColor];  
      break;  
    case CLProximityFar:  
      message = @"Shiver me timbers. I be more than a long John away";  
      bgColor = [UIColor colorWithRed:.0f green:.0f blue:230.0f alpha:1.0f];  
      break;  
    case CLProximityNear:  
      message = @"Argh. I be no further than a hornpipe away";  
      bgColor = [UIColor orangeColor];  
      break;  
    case CLProximityImmediate:  
    default:  
      message = @"Smartly does it lass. I be richer than a pig in grog";  
      bgColor = [UIColor redColor];  
      break;  
  }  
  if (beacon.proximity != self.previousProximity) {  
    [self speak:message];  
    [self.statusLabel setText:message];  
    [self.view setBackgroundColor:bgColor];  
    self.previousProximity = beacon.proximity;  
  }  
}  

What we do here is switch on the beacon proximity and prepare a background colour and message based on how close the beacon is. If the proximity has changed since last time then we set the view's background colour, the label text and then we speak the message using an AVSpeechSynthesizer. To do this however we need the speak method.

-(void)speak:(NSString*)message {  
  AVSpeechSynthesizer * synth = [[AVSpeechSynthesizer alloc] init];  
  AVSpeechUtterance * utterance = [[AVSpeechUtterance alloc] initWithString:message];  
  [utterance setRate:AVSpeechUtteranceMaximumSpeechRate *.3f];  
  [utterance setVolume:1.0f];  
  [utterance setPitchMultiplier:0.7f];  
  [utterance setVoice:[AVSpeechSynthesisVoice voiceWithLanguage:@"en-IE"]];  
  [synth speakUtterance:utterance];  
} 

I decided that the en-GB implementation of the AVSpeechSynthesizer could never sound remotely pirate-like. He was far too posh and so I took the Irish voice, lowered the pitch and speed and probably got the closest I could to a pirate using the english speaking voices on the iPhone.

We're almost done. The only thing we've overlooked is the fact that we're not doing anything with the local notifications which get raised when the hunter enters a new beacon region. Head on over to the app delegate and add the following code:

-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {  
  UIAlertView * av = [[UIAlertView alloc] initWithTitle:@"Find the treasure"  
                           message:notification.alertBody  
                           delegate:NULL  
                      cancelButtonTitle:@"OK"  
                      otherButtonTitles:nil, nil];  
  [av show];  
}  

Now we're presenting a UIAlertView when the app is running and the user enters a new region.

Now we're done

I hope you've had fun playing with iBeacon. The technology has the potential to revolutionize the way we shop, interact and behave in different geolocation settings.

Download the source code here.

Republished from CreatedInEden.com

0 comments


Or enter your name and Email
No comments have been posted yet.