Day 75 - 100 Days of Swift

4 minute read

Project 22 (part 1)

Day 75 is the first part of the twenty-second project. It is a project where you set up an app to detect nearby iBeacons and give the user feedback on how far away it is.

First, you add a few strings to Info.plist to let the user know what you’ll be using their location for:

1
2
3
4
<key>NSLocationWhenInUseUsageDescription</key>
<string>$(PRODUCT_NAME) needs your location to help you find the nearest beacon.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>$(PRODUCT_NAME) needs your location to help you find the nearest beacon.</string>

The “always” permission would let the system wake up your app when it detects a beacon, even if it isn’t running, but it requires a lot more trust from the user. The “when in use” permission allows you to detect beacons when the app is running, but you need permission strings for both.

Then you lay out the storyboard with a big label in the middle where we’ll let the user know how far away the beacon is:

Screenshot of storyboard layout

Then, in ViewController.swift you import CoreLocation, adopt the delegate protocol for CLLocationManager, and ask the user for location permission:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    var locationManager: CLLocationManager?

    @IBOutlet var distanceLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.requestAlwaysAuthorization()

        view.backgroundColor = .gray
    }
}

Then, when the authorization status changes we’ll be notified via a delegate method:

1
2
3
4
5
6
7
8
9
10
func locationManager(_ manager: CLLocationManager,
                     didChangeAuthorization status: CLAuthorizationStatus) {
    if status == .authorizedAlways {
        if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) {
            if CLLocationManager.isRangingAvailable() {
                startScanning()
            }
        }
    }
}

And there, if we have all the necessary permissions and access, we call startScanning which looks like this:

1
2
3
4
5
6
7
8
9
10
private 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)
}

Here, we’re hardcoding a UUID, as well as major and minor identifiers just to make sure we’re only detecting the test beacon we set up with a second iOS device via the Locate Beacon app, and then telling the locationManager to monitor and range for it.

Now, assuming we have permission, we’re receiving information about the beacon, but we’re not doing anything with it. So next we add another helper method which will update our UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private func update(distance: CLProximity) {
    UIView.animate(withDuration: 0.5) {
        switch distance {
        case .far:
            self.view.backgroundColor = .blue
            self.distanceLabel.text = "FAR"
        case .near:
            self.view.backgroundColor = .orange
            self.distanceLabel.text = "NEAR"
        case .immediate:
            self.view.backgroundColor = .red
            self.distanceLabel.text = "RIGHT HERE"
        default:
            self.view.backgroundColor = .gray
            self.distanceLabel.text = "UNKNOWN"
        }
    }
}

And then call it from another delegate method where receive updates about the beacons we’re ranging for:

1
2
3
4
5
6
7
8
9
func locationManager(_ manager: CLLocationManager,
                     didRangeBeacons beacons: [CLBeacon],
                     in region: CLBeaconRegion) {
    if let beacon = beacons.first {
        update(distance: beacon.proximity)
    } else {
        update(distance: .unknown)
    }
}

Finally, I just made a few tweaks to make the UI look a little nicer:

1
2
3
4
5
6
override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
}

// In viewDidLoad
distanceLabel.textColor = .white

With that, the app works. It is pretty magical that some simple little code like this can update the UI on your phone to reflect how far you are away from an iBeacon. As I walk around my house with my phone in my hand and the iPad sitting in a corner, my phone almost immediate updates to reflect the distance. It looks like this:

Screenshot of working app

You can find my version of this project at the end of day 75 on Github here.