最近因為工作上的需求,需要透過 app 與支援 iBeacon 的裝置溝通,所以整理了一些筆記。在 iOS 的世界,app 可以作為 iBeacon 的接收者,也可以讓裝置作為 iBeacon 的發送者,這裡我只紀錄前者(接收者)的開發。

開發步驟

1. 定位服務的設定

iBeacon 的接收需要使用到定位服務,所以首先需要做以下這些設定:

  1. 加入 CoreLocation.framework
  2. Info.plist 加入 NSLocationWhenInUseUsageDescription key,如果需要在背景偵測是否有進入或離開 iBeacon 範圍,就要另外加入 NSLocationAlwaysUsageDescription key
  3. 在程式碼加入 import CoreLocation

2. 開始定位 iBeacon

簡化起見,我們把程式碼放在 ViewController 裡頭,以下程式碼即可開始偵測 iBeacon。

import CoreLocation

class ViewController: UIViewController {
    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.requestAlwaysAuthorization()
        locationManager.delegate = self

        let uuid = UUID(uuidString: "12345678-1234-1234-1234-123456789ABC")!
        let region = CLBeaconRegion(proximityUUID: uuid, identifier: "iBeacon Region")
        locationManager.startMonitoring(for: region)
    }
}

每個 iBeacon 裝置都會廣播三種資料用來作為裝置辨識,分別是 UUIDmajorminor。舉個常見的場景:假設我是某店家,我可以把所有佈置的 iBeacon 裝置都設定同一組 UUID,然後設定不同的 major 代表這個裝置放在哪棟建築物,minor 代表這個裝置的編號。當我開發的 app 連上裝置後,就可以透過 majorminor 得知目前的位置,藉此提供給使用者相關的資訊。

在上述的程式碼,我設定的 region 只有 UUID,代表符合這個 UUID 的裝置都可以被偵測。根據需求不同,也可以額外設定 majorminor

locationManager 呼叫 startMonitoring(for:) 就會開始偵測 iBeacon 裝置,然後把結果回報給 delegate,所以接下來我們要處理 delegate callbacks。

3. 處理 delegate callbacks

我們把 callbacks 放在 extension ViewController: CLLocationManagerDelegate { } 裡頭,首先來處理錯誤。

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("Location manager did fail: \(error.localizedDescription)")
}
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
    print("Location manager monitoring did fail: \(error.localizedDescription)")
}

再來處理定位範圍。

func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    guard region is CLBeaconRegion else { return }
    // 在這裡做一些進入 region 的處理,例如提供一些提示
    guard CLLocationManager.isRangingAvailable() else { return }
    // 既然進入 region 了,那就偵測跟裝置的距離
    manager.startRangingBeacons(in: region as! CLBeaconRegion)
}

func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    guard region is CLBeaconRegion else { return }
    // 在這裡做一些離開 region 的處理,例如提供一些提示
    guard CLLocationManager.isRangingAvailable() else { return }
    // 既然離開 region 了,那就停止偵測跟裝置的距離
    manager.stopRangingBeacons(in: region as! CLBeaconRegion)
}

上述兩個 callbacks 只有在進入跟離開範圍時,才會被呼叫。如果啟動 app 的時候已經在 iBeacon 裝置範圍內,則不會收到進入範圍的通知,所以我們需要額外的處理。

func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
    // 開始偵測範圍之後,就先檢查目前的 state 是否在範圍內
    manager.requestState(for: region)
}

func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
    guard region is CLBeaconRegion else { return }

    if state == .inside { // 在範圍內
        if CLLocationManager.isRangingAvailable() {
            manager.startRangingBeacons(in: region as! CLBeaconRegion)
        }
    } else if state == .outside { // 在範圍外
        if CLLocationManager.isRangingAvailable() {
            manager.stopRangingBeacons(in: region as! CLBeaconRegion)
        }
    }
}

最後來處理定位距離。

func locationManager(_ manager: CLLocationManager, rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) {
    print("Location manager ranging beacons did fail: \(error.localizedDescription)")
}

func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
    // beacons 是一個 array,裡頭的 beacon 由近到遠排序
    if let beacon = beacons.first {
        // 取得距離最近的 beacon 了,作些事情吧
    }
}

4. 處理背景偵測

如果我們需要在沒有啟動 app 的時候,也能收到進入或離開 beacon 範圍的通知,那我們需要在 AppDelegate 做一些設定。

import CoreLocation

class AppDelegate: UIResponder, UIApplicationDelegate {
    let locationManager = CLLocationManager()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (granted, error) in
        }
        locationManager.delegate = self
        return true
    }
}

extension AppDelegate: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        guard region is CLBeaconRegion else { return }

        let content = UNMutableNotificationContent()
        content.title = "iBeacon Demo"
        content.body = "You enter a region"
        content.sound = .default

        let request = UNNotificationRequest(identifier: "Demo", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }

    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        guard region is CLBeaconRegion else { return }

        let content = UNMutableNotificationContent()
        content.title = "iBeacon Demo"
        content.body = "You exit a region"
        content.sound = .default

        let request = UNNotificationRequest(identifier: "Demo", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
    }
}

注意事項

  • 需要實體機器開發 iBeacon app,Simulator 有諸多不便。
  • Beacon 是單向廣播,app 只能接收,不能跟 beacon 溝通。廣播的資料只有 UUIDmajorminor,沒有其他資訊。app 收到這些資訊之後需要自行發揮創意,決定要做什麽事(例如可以打 API 跟 server 查詢這組 major + minor 代表什麼地點)。
  • UUID 的格式是 12345678-1234-1234-1234-123456789ABC,16進位字串,大小寫不拘。
  • 最多只能 monitor 20 個 beacon,想要更多的話要自己手動管理。
  • 記得要檢查 isMonitoringAvailable(for:)isRangingAvailable()
  • 只有在進入或離開 region 才會觸發通知,在 region 內任何的移動並不會造成持續一直接收通知。
  • 要偵測 beacon,需要 CoreLocation,要作爲 beacon,需要 CoreBluetooth
  • 如果只是需要使用者位置,用 NSLocationWhenInUseUsageDescription key,呼叫 requestWhenInUseAuthorization();如果需要在背景接收 notification,除了這個 key 還要加上 NSLocationAlwaysUsageDescription key,呼叫 requestAlwaysAuthorization()
  • Monitoring 用來偵測是否在 beacon 範圍内;Ranging 用來判斷跟 beacon 的距離遠近。所以先用 monitor,在範圍内才啓動 ranging,ranging 很耗電,所以不要在背景做這件事。

參考資料