Tutorial: Swift based iBeacon App Development with CoreLocation on Apple iOS 7/8 July 02 2014 25 Comments

We've had a number of customers who want to use Apple's new programming language Swift to develop iBeacon applications, so we decided to develop a tutorial to make it easier to get up and running. Right now, Swift, Xcode 6, and iOS 8 are all covered under Apple's developer program NDA. It is our understanding, however, that Apple now supports the release of tutorials such as this, provided that they are limited to code samples and do not reveal screenshots of technology covered under NDA. We have screenshots for this tutorial and we'll release them as soon as the NDA status allows us to do so. In the meantime, we've tried to make it clear enough to follow along in this text-only format. If anything is unclear, feel free to start a discussion below in the comments.

If you've landed here by accident and would prefer to work in Objective-C, no problem. We have a very similar tutorial that will walk you through building the same app, step-by-step in Objective-C.

If you need iBeacon hardware, take a look at our selection. We offer two different iBeacon transmitter modules, both with fast (same-day on orders placed before 3pm eastern time), free first-class shipping from Brooklyn, NY to anywhere in the world! If you don't see what you need, drop us a line at sales@ibeaconmodules.us.

Once you have your hardware, be sure to visit the product pages for simple activation instructions before using your beacons for the first time.

If all this is over your head, but you still have an idea for using iBeacon technology, contact our development team for information and pricing on custom application design and development services. Otherwise, let's get going!

First, you'll need to register for the iOS developer program and provision a device. Our previous tutorial covers these initial steps, so if you need help, go check it out and come back here when you're ready to dive in.

You'll also need to download and install the Xcode 6 beta, which is available from the iOS Dev Center once you've registered with the iOS developer program.

Once you've got a device provisioned and Xcode 6 up and running, you're ready to create a new project!

 

Swift-based iBeacon Template Application Tutorial

 

You can download the template project from GitHub, but sometimes it's more illuminating to walk through the steps yourself:

 

1. Create a Swift Project

From the File menu, select New->Project. Select IOS Application from the left menu, and Single View Application from the options on the right. Then click Next. 

2. Configure Project Options

In the next dialog, configure a project name, organization name, and organization identifier. Be sure to select Swift as the language. We'll be using iPhone as the device.

3. Add the CoreLocation framework to the project

Click on the main project entry in the file browser. Then make sure the General tab is selected. Scroll to the bottom and click the + to add a Linked Framework. Then find CoreLocation.framework in the list, select it, and click the Add button.

While you're in the General settings, you might want to change the Deployment target to 7.0 if you want this app to run on older versions of iOS. 6.x and earlier won't work, because they don't have the necessary CoreLocation functionality and they are generally reported to not work with Swift.

4. Configure Background Capabilities

In order to perform background ranging and monitoring and receive notifications, your app needs to register its need to use Location updates and Bluetooth LE accessories in the background. To do this, click the main project entry in the file browser. Then select the Capabilities tab and find Background Modes. Turn the switch ON, and then check Location updates and Uses Bluetooth LE accessories.

5. Define a region to be monitored

For now, we're going to set things up in the application:didFinishLaunchingWithOptions method in AppDelegate.swift. First, we need to import the CoreLocation header, and then we will set up a CLBeaconRegion with the UUID for our beacon(s). A UUID is just an identifier that identifies a beacon or set of beacons. Any beacons you buy from us will come with the same UUID, although you can reprogram this if you like. This tutorial assumes that you've left the UUID at its default setting. Beacons can also be differentiated by another pair of IDs, called major and minor, but we'll get to that at another time. You also need to define an identifier for the region. This will let you distinguish between sets of beacons should your application ever track more than one. For now, the top part of your App Delegate implementation should look like this:

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
                            
    var window: UIWindow?


    func application(application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {

        let uuidString = "EBEFD083-70A2-47C8-9837-E7B5634DF524"
        let beaconIdentifier = "iBeaconModules.us"
        let beaconUUID:NSUUID = NSUUID(UUIDString: uuidString)
        let beaconRegion:CLBeaconRegion = CLBeaconRegion(proximityUUID: beaconUUID,
            identifier: beaconIdentifier)
        
        return true
    }

6. Set up plist entry for LocationAlways

iOS 8 requires special permission for always-on location monitoring, which includes iBeacon ranging. You need to include a specific plist entry that enables iOS to tell the user why you want this permission.

In the file browser under Supporting Files, open Info.plist. Select the top row "Information Property List". Click the small plus sign to Add a Row. Give it the key (in the left column) NSLocationAlwaysUsageDescription. Make sure the type is set to String, and then set its value to whatever string you'd like to use to inform the user when they are prompted for location permissions.

7. Set up CLLocationManager for foreground/background monitoring and ranging

Now we have a region defined, but it won't do anything until we tell the device to monitor it. To do this, we need to create an instance of CLLocationManager and tell it to start monitoring the region we just created. We also need to define a "delegate" which is an object that gets notified whenever something happens with the location manager. We'll just use the App Delegate for now.

First, we need to import CoreLocation headers. Then we need to tell the compiler that our AppDelegate class will implement the CLLocationManagerDelegate protocol, and declare a property to hold our CLLocationManager object so that it survives for the entire lifetime of the application. We declare it with a question mark at the end of the line because it optional. This is because it's empty when the AppDelegate object is created and gets created in the didFinishLaunchingWithOptions method. Because it's optional, we have to access it with an exclamation mark after its name when we use it, as shown in the code below.

Inside the didFinishLaunchingWithOptions method, we need to create the CLLocationManager instance. Then we need to request "Always" authorization for location information. This is a new method in iOS 8 so we'll first check to see if the CLLocationManager responds to the selector and only call it if it does.

Then we tell it to start monitoring the region, start ranging the beacons, and start updating location. All three of these are required to receive range information updates when the app is in the background. After this is all complete, the top of AppDelegate.swift should look like this:

import UIKit
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
                            
    var window: UIWindow?
    var locationManager: CLLocationManager?


    func application(application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
        let uuidString = "EBEFD083-70A2-47C8-9837-E7B5634DF524"
        let beaconIdentifier = "iBeaconModules.us"
        let beaconUUID:NSUUID = NSUUID(UUIDString: uuidString)
        let beaconRegion:CLBeaconRegion = CLBeaconRegion(proximityUUID: beaconUUID,
            identifier: beaconIdentifier)
            
        locationManager = CLLocationManager()
        if(locationManager!.respondsToSelector("requestAlwaysAuthorization")) {
            locationManager!.requestAlwaysAuthorization()
        }
        locationManager!.delegate = self
        locationManager!.pausesLocationUpdatesAutomatically = false
        
        locationManager!.startMonitoringForRegion(beaconRegion)
        locationManager!.startRangingBeaconsInRegion(beaconRegion)
        locationManager!.startUpdatingLocation()
        
        return true
    }

8. Define what happens when the region is entered

The device is now monitoring the beacon, but we still haven't told it what to do when it sees it. Because we set our App Delegate as the CLLocationManager instance's delegate back in the previous step, it's going to look for methods here to call when various events happen. You can find a list of methods in the CLLocationManagerDelegate Protocol Reference.

locationManager:didRangeBeacons:inRegion: looks interesting. Let's tell it to do something. In Swift, delegate protocols are typically implemented as class extensions.

Let's write an extension with a method to send a local notification and then handle didRangeBeacons and notify the user of the current status of the nearest beacon. Add this to the bottom of AppDelegate.swift (at the very bottom, outside of the closing bracket for the existing class declaration):

extension AppDelegate: CLLocationManagerDelegate {
    func sendLocalNotificationWithMessage(message: String!) {
        let notification:UILocalNotification = UILocalNotification()
        notification.alertBody = message
        UIApplication.sharedApplication().scheduleLocalNotification(notification)
    }
    
    func locationManager(manager: CLLocationManager!,
        didRangeBeacons beacons: AnyObject[]!,
        inRegion region: CLBeaconRegion!) {
            NSLog("didRangeBeacons");
            var message:String = ""
            
            if(beacons.count > 0) {
                let nearestBeacon:CLBeacon = beacons[0] as CLBeacon
                
                switch nearestBeacon.proximity {
                case CLProximity.Far:
                    message = "You are far away from the beacon"
                case CLProximity.Near:
                    message = "You are near the beacon"
                case CLProximity.Immediate:
                    message = "You are in the immediate proximity of the beacon"
                case CLProximity.Unknown:
                    return
                }
            } else {
                message = "No beacons are nearby"
            }
            
            NSLog("%@", message)
            sendLocalNotificationWithMessage(message)
    }
}

If you run this on iOS 8, you'll see a bunch of warnings in the Xcode log saying that your app hasn't requested permission to send notifications. This is a new iOS 8 requirement. Fortunately, it's pretty simple to do. We just need to add the following code to the application:didFinishLaunchingWithOptions: method. Anywhere should be fine, but I put it at the bottom, just before the return statement.

        if(application.respondsToSelector("registerUserNotificationSettings:")) {
            application.registerUserNotificationSettings(
                UIUserNotificationSettings(
                    forTypes: UIUserNotificationType.Alert | UIUserNotificationType.Sound,
                    categories: nil
                )
            )
        }

It checks to see if the UIApplication object responds to the registerUserNotificationSettings selector, so the app will still function as expected on iOS 7. However, if we're on iOS 8, it will request permission from the user to send notifications. Since this is just an example focusing on the iBeacon functionality, we're not handling the case where this is declined, but in a production app, this would be a good thing to do.

Now if you run the app, nothing will happen when it's foregrounded (we'll deal with that in a bit), but if you background it (press the home button) while in the presence of an iBeacon, you should see a steady stream of notifications letting you know your proximity to the beacon.

9. Clean Up

This will work as is, but let's tidy things up just a bit. Since we've implemented background ranging with background notifications, you'll get a lot of redundant alerts (one per second while the app is in the background), so we should modify things to only alert us when the range changes. First, we need to add a place to store the last proximity reading. Add the following property to the top of AppDelegate.swift, just below the declaration of the window and locationManager:

    var lastProximity: CLProximity?

Then, down in the didRangeBeacons method implementation, add this just before the switch statement that checks the proximity of the nearest beacon:

                if(nearestBeacon.proximity == lastProximity ||
                    nearestBeacon.proximity == CLProximity.Unknown) {
                        return;
                }
                lastProximity = nearestBeacon.proximity;

If the proximity hasn't changed or is temporarily unknown, the method returns and doesn't re-alert us.

That's great, but now what happens if we leave the beacon range entirely? The device will keep trying to range beacons that are no longer in range which just wastes power. Luckily, the CLLocationManager will tell us via a different method when we have entered and exit the range, and we can start and stop the more detailed ranging operations accordingly. Add these methods to the CLLocationManagerDelegate extension to our App Delegate (just below the implementation of didRangeBeacons):

    func locationManager(manager: CLLocationManager!,
        didEnterRegion region: CLRegion!) {
            manager.startRangingBeaconsInRegion(region as CLBeaconRegion)
            manager.startUpdatingLocation()
            
            NSLog("You entered the region")
            sendLocalNotificationWithMessage("You entered the region")
    }
    
    func locationManager(manager: CLLocationManager!,
        didExitRegion region: CLRegion!) {
            manager.stopRangingBeaconsInRegion(region as CLBeaconRegion)
            manager.stopUpdatingLocation()
            
            NSLog("You exited the region")
            sendLocalNotificationWithMessage("You exited the region")
    }

This also logs and alerts us of the system-level region entry/exit.

 

10. Foreground Display

(This part is greatly helped by screenshots, which we can't post yet because of the Apple NDA. In the meantime, you might find it helpful to refer back to our Objective-C-based tutorial which has screenshots. The two are very similar here in terms of the Storyboard configuration.)

OK, things are great in background mode, but when the app is foregrounded, all we have is a plain white screen and no indication of what's happening. Let's fix that by adding a UITableView and populating it with information about all of the beacons in range.

Open the Main.storyboard file. Then select the main view to give it focus. Then, in the list in the bottom right (you may need to use the buttons in the top right corner of the screen to open the tray on the right side of the window), find a Table View (not a Table View Controller, since we're just using the existing view controller for now to keep things simple). Drag it onto the view and position and scale it so that it takes up the entire view except the area taken up by the status bar at the top.

Now we need to declare a variable to hold a reference to this UITableView. We'll use the main view controller (ViewController.swift). At the top of the class declaration, inside the brackets but before any of the function declarations, add this:

    @IBOutlet var tableView: UITableView

Next, open the storyboard file again. Then option-click (or right-click) on the table view you just added. In the context menu that pops up, drag from the little circle to the right of dataSource over to the View Controller entry in the left portion of the storyboard view.

Do the same for delegate and New Referencing Outlet. When you drop the connection for New Referencing Outlet, you'll get a pop-up asking which variable the table view should map to. Select tableView, which matches the name of the property we just created.

Now we need to declare our View Controller to implement the protocols required by the above connections, UITableViewDataSource and UITableViewDelegate. Add them to the declaration of your view controller class near the top of ViewController.swift, like this:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

Now declare an array of CLBeacons at the top of the ViewController class declaration (you'll also need to import CoreLocation into ViewController.swift to do this):

    var beacons: CLBeacon[]

When the beacons are ranged in the app delegate, now we need to pass the array of beacons to the View Controller and tell the table view to update itself. Add the following code to the top of locationManager:didRangeBeacons:inRegion in the App Delegate CLLocationManagerDelegate extension:

            let viewController:ViewController = window!.rootViewController as ViewController
            viewController.beacons = beacons as CLBeacon[]?
            viewController.tableView.reloadData()

Whenever beacons are ranged, they are passed to the view controller, and the tableView is reloaded with the new data.

Now we just need to implement -tableView:numberOfRowsInSection: and -tableView:cellForRowAtIndexPath: in our View Controller. These methods are called by the table view so it can update itself with the latest data. As with the CLLocationManagerDelegate methods, we'll implement these in class extensions:

extension ViewController: UITableViewDataSource {
    func tableView(tableView: UITableView!,
        numberOfRowsInSection section: Int) -> Int {
            if(beacons != nil) {
                return beacons!.count
            } else {
                return 0
            }
    }
    
    func tableView(tableView: UITableView!,
        cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
            var cell:UITableViewCell? = 
tableView.dequeueReusableCellWithIdentifier("MyIdentifier") as? UITableViewCell if(cell == nil) { cell = UITableViewCell(style: UITableViewCellStyle.Subtitle,
reuseIdentifier: "MyIdentifier") cell!.selectionStyle = UITableViewCellSelectionStyle.None } let beacon:CLBeacon = beacons![indexPath.row] var proximityLabel:String! = "" switch beacon.proximity { case CLProximity.Far: proximityLabel = "Far" case CLProximity.Near: proximityLabel = "Near" case CLProximity.Immediate: proximityLabel = "Immediate" case CLProximity.Unknown: proximityLabel = "Unknown" } cell!.textLabel.text = proximityLabel let detailLabel:String = "Major: \(beacon.major.integerValue), " + "Minor: \(beacon.minor.integerValue), " + "RSSI: \(beacon.rssi as Int), " + "UUID: \(beacon.proximityUUID.UUIDString)" cell!.detailTextLabel.text = detailLabel return cell } } extension ViewController: UITableViewDelegate { }

Since we're not doing any cell selection or other functionality that would require delegate callbacks, our UITableViewDelegate extension is empty for now, but that's where you'd start implementing that functionality when you're ready.

Conclusion

 

Now we've built a iBeacon app using Swift that can range beacons in the foreground and background and display the data and push alerts, so we're going to stop there for now. You can also download the entire project from GitHub if you'd rather not build it up piece by piece. I hope this helps you get up and running and hopefully understand a bit more about iBeacon and how it's used with iOS 7/8. If you have questions or feedback on this tutorial, leave us a comment!

If you need iBeacon hardware, take a look at our catalog. We offer a range of iBeacon transmitter modules, all with fast, free domestic shipping within the U.S. If you don't see what you need, drop us a line at sales@ibeaconmodules.us.