Day 76 - 100 Days of Swift

4 minute read

Project 22 (part 2)

Day 76 is the second part of the twenty second project. You review what you learned yesterday and he gives you a few challenges to extend the beacon app. He challenges you to present an alert when a beacon is detected. He challenges you to monitor for multiple different beacons and to display which one is currently being ranged to the user. And he challenges you to add a circle to the view that reflects how close the beacon is to the user.

For the first challenge I added a helper function to present the alert and a variable to keep track of whether it had been presented yet or not:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private func presentAlert() {
    hasAlertBeenShown = true
    let alertController = UIAlertController(title: "Found a beacon",
                                            message: nil,
                                            preferredStyle: .alert)
    let action = UIAlertAction(title: "Okay",
                               style: .default)
    alertController.addAction(action)

    present(alertController, animated: true)
}

private var hasAlertBeenShown = false

// In update(distance:)
if distance != .unknown && !hasAlertBeenShown { presentAlert() }

For the second I reorganized things a little bit because I found didRangeBeacons was getting called multiple times when I was ranging for multiple beacon regions, so the UI was going crazy. First, I made a dictionary of UUIDs and Identifiers so they were easier to keep track of:

1
2
3
4
5
private var uuids = [
    UUID(uuidString: "5A4BCFCE-174E-4BAC-A814-092E77F6B7E5")!: "My Beacon",
    UUID(uuidString: "74278BDA-B644-4520-8F0C-720EAF059935")!: "Apple AirLocate",
    UUID(uuidString: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")!: "Other Apple AirLocate"
]

Then I updated startScanning to use that instead of the one hard-coded UUID:

1
2
3
4
5
6
7
8
9
private func startScanning() {
    for (uuid, identifier) in uuids {
        let beaconRegion = CLBeaconRegion(proximityUUID: uuid,
                                          identifier: identifier)

        locationManager?.startMonitoring(for: beaconRegion)
    }

}

You’ll notice that .startRangingBeacons isn’t there anymore. That is because I only want to range for beacons that are in the current region, so I don’t receive unnecessary updates from didRangeBeacons. I used a couple of other CLLocationManagerDelegate methods for that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private var currentRegion: CLBeaconRegion?

func locationManager(_ manager: CLLocationManager,
                     didEnterRegion region: CLRegion) {

    if let beaconRegion = region as? CLBeaconRegion {
        if let current = currentRegion {
            locationManager?.stopRangingBeacons(in: current)
        }
        locationManager?.startRangingBeacons(in: beaconRegion)
        currentRegion = beaconRegion
    }
}

func locationManager(_ manager: CLLocationManager,
                     didExitRegion region: CLRegion) {
    if let beaconRegion = region as? CLBeaconRegion {
        locationManager?.stopRangingBeacons(in: beaconRegion)
        update(distance: .unknown, name: "")
    }
}

Then I added a beaconLabel to the storyboard and updated the update(distance:) method to also take in a name string:

1
2
3
4
5
6
7
private func update(distance: CLProximity, name: String) {
self.beaconLabel.text = name
// in each case
self.beaconLabel.transform = .identity

// In the default case
self.beaconLabel.transform = CGAffineTransform.identity.scaledBy(x: 1, y: 0.001)

This sets the name and animates it in/out as appropriate.

And then passed that name in didRangeBeacons:

1
2
let name = uuids[beacon.proximityUUID] ?? "Unknown Beacon"
update(distance: beacon.proximity, name: name)

For the third challenge, I just added an empty view to the storyboard, constrained it to sit above the labels and to be 256 x 256. Then I added similar animations to it in update:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// In viewDidLoad
circleView.layer.cornerRadius = 128
circleView.backgroundColor = .white
circleView.transform = circleView.transform.scaledBy(x: 0.001, y: 0.001)

// In update(distance:name:)
// far case
self.circleView.transform = CGAffineTransform.identity.scaledBy(x: 0.25, y: 0.25)

// near case
self.circleView.transform = CGAffineTransform.identity.scaledBy(x: 0.5, y: 0.5)

// immediate case
self.circleView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: 1)

// default case
self.circleView.transform = CGAffineTransform.identity.scaledBy(x: 0.001, y: 0.001)

And with that, it is finished. It looks like this:

Screenshots of working app

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