最近因為工作上的需求,需要透過 app 與支援 iBeacon 的裝置溝通,所以整理了一些筆記。在 iOS 的世界,app 可以作為 iBeacon 的接收者,也可以讓裝置作為 iBeacon 的發送者,這裡我只紀錄前者(接收者)的開發。
開發步驟
1. 定位服務的設定
iBeacon 的接收需要使用到定位服務,所以首先需要做以下這些設定:
- 加入
CoreLocation.framework
- 在
Info.plist
加入NSLocationWhenInUseUsageDescription
key,如果需要在背景偵測是否有進入或離開 iBeacon 範圍,就要另外加入NSLocationAlwaysUsageDescription
key - 在程式碼加入
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 裝置都會廣播三種資料用來作為裝置辨識,分別是 UUID
、major
、minor
。舉個常見的場景:假設我是某店家,我可以把所有佈置的 iBeacon 裝置都設定同一組 UUID
,然後設定不同的 major
代表這個裝置放在哪棟建築物,minor
代表這個裝置的編號。當我開發的 app 連上裝置後,就可以透過 major
跟 minor
得知目前的位置,藉此提供給使用者相關的資訊。
在上述的程式碼,我設定的 region 只有 UUID
,代表符合這個 UUID 的裝置都可以被偵測。根據需求不同,也可以額外設定 major
或 minor
。
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 溝通。廣播的資料只有
UUID
、major
、minor
,沒有其他資訊。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 很耗電,所以不要在背景做這件事。