NEW! Pre-order my latest book, Testing Swift! >>

< Previous: Requesting location: Core Location   Next: Wrap up >

Hunting the beacon: CLBeaconRegion

If everything is working, you should have received a large iOS confirmation prompt asking whether you grant the user access to their location. This message is really blunt, so users hopefully take a few moments to read it before continuing.

But that prompt is not the only way iOS helps users guard their privacy. If you went for "when in use", you'll still get location information while your app is in the background if you enable the background capability, and iOS will notify users that this is happening by making the device status bar blue and saying "YourAppName is using your location." If you went for "always", iOS will wait a few days then ask the user if they still want to grant permission, just to be fully sure.

Assuming everything went well, let's take a look at how we actually range beacons. First, we use a new class called CLBeaconRegion, which is used to identify a beacon uniquely. Second, we give that to our CLLocationManager object by calling its startMonitoring(for:) and startRangingBeacons(in:) methods. Once that's done, we sit and wait. As soon as iOS has anything tell us, it will do so.

iBeacons are identified using three pieces of information: a universally unique identifier (UUID), plus a major number and a minor number. The first number is a long hexadecimal string that you can create by running the uuidgen in your Mac's terminal. It should identify you or your store chain uniquely.

The major number is used to subdivide within the UUID. So, if you have 10,000 stores in your supermarket chain, you would use the same UUID for them all but give each one a different major number. That major number must be between 1 and 65535, which is enough to identify every McDonalds and Starbucks outlet combined!

The minor number can (if you wish) be used to subdivide within the major number. For example, if your flagship London store has 12 floors each of which has 10 departments, you would assign each of them a different minor number.

The combination of all three identify the user's precise location:

  • UUID: You're in a Acme Hardware Supplies store.
  • Major: You're in the Glasgow branch.
  • Minor: You're in the shoe department on the third floor.

If you don't need that level of detail you can skip minor or even major – it's down to you.

It's time to put this into code, so we're going to create a new method called startScanning() that contains the following:

func startScanning() {
    let uuid = UUID(uuidString: "5A4BCFCE-174E-4BAC-A814-092E77F6B7E5")!
    let beaconRegion = CLBeaconRegion(proximityUUID: uuid, major: 123, minor: 456, identifier: "MyBeacon")

    locationManager.startMonitoring(for: beaconRegion)
    locationManager.startRangingBeacons(in: beaconRegion)
}

You met UUID in project 10, but here we're converting a string into a UUID rather than generating a UUID and converting it to a string. The UUID I'm using there is one of the ones that comes built into the Locate Beacon app – look under "Apple AirLocate 5A4BCFCE" and find it there. Note that I'm scanning for specific major and minor numbers, so please enter those into your Locate Beacon app.

The identifier field is just a string you can set to help identify this beacon in a human-readable way. That, plus the UUID, major and minor fields, goes into the CLBeaconRegion class, which is used to identify and work with iBeacons. It then gets sent to our location manager, asking it to monitor for the existence of the region and also to start measuring the distance between us and the beacon.

Find the // do stuff comment inside the didChangeAuthorization method you wrote a few minutes ago, and change it to this:

startScanning()

That method should now be much clearer: we only start scanning for beacons when we have permission and if the device is able to do so.

If you run the app now (on a real device, remember!) you'll see that it literally looks identical, as if we needn't have bothered writing any iBeacon code. But behind the scenes, detection and ranging is happening, we're just not doing anything with it!

This app is going to change the label text and view background color to reflect proximity to the beacon we're scanning for. This will be done in a single method, called update(distance:), which will use a switch/case block and animations in order to make the transition look smooth. Let's write that method first:

func update(distance: CLProximity) {
    UIView.animate(withDuration: 0.8) { [unowned self] in
        switch distance {
        case .unknown:
            self.view.backgroundColor = UIColor.gray
            self.distanceReading.text = "UNKNOWN"

        case .far:
            self.view.backgroundColor = UIColor.blue
            self.distanceReading.text = "FAR"

        case .near:
            self.view.backgroundColor = UIColor.orange
            self.distanceReading.text = "NEAR"

        case .immediate:
            self.view.backgroundColor = UIColor.red
            self.distanceReading.text = "RIGHT HERE"
        }
    }
}

Most of that is just choosing the right color and text, but you'll notice the method accepts a CLProximity as its parameter. This can only be be one of our four distance values, which is why we don't need a default case in there – Swift can see the switch/case is complete.

With that method written, all that remains before our project is complete is to catch the ranging method from CLLocationManager. We'll be given the array of beacons it found for a given region, which allows for cases where there are multiple beacons transmitting the same UUID.

If we receive any beacons from this method, we'll pull out the first one and use its proximity property to call our update(distance:) method and redraw the user interface. If there aren't any beacons, we'll just use .unknown, which will switch the text back to "UNKNOWN" and make the background color gray.

Here's the code:

func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
    if beacons.count > 0 {
        let beacon = beacons[0]
        update(distance: beacon.proximity)
    } else {
        update(distance: .unknown)
    }
}

With that, your code is done. Run it on a device, make sure Locate Beacon is up and transmitting, and enjoy your location-aware app!

Upgrade your apps!

Take on Core ML, ARKit, PDFKit, Core NFC, and more all in one book that's packed with practical projects!

< Previous: Requesting location: Core Location   Next: Wrap up >
MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>