Swift iBeacon App 程式設計
iBeacon 是一種可以發射藍芽訊號的裝置,利用它結合 iOS App 我們可以實現許多有趣的應用。它最主要的應用有以下兩種:
- 偵測 iPhone 進入某個 iBeacon 的範圍時做某件事。
- 偵測 iPhone 跟 iBeacon 的距離,依不同距離做不同的事情。
比方彼得潘和溫蒂逛著博物館,走到名畫蒙娜麗莎前,App 偵測到裝在蒙娜麗莎身上的 iBeacon,於是 App 開始唸出蒙娜麗莎的相關介紹,透過 AirPods 傳到彼得潘的耳朵,幫助彼得潘在溫蒂面前變成博學的文青。
想開發偵測 iBeacon 的 iOS App 並不難,但首先我們得先買個 iBeacon 來實驗,比方以下網頁裡可看到玲琅滿目的 iBeacon。
先別急著買,雖然 iBeacon 不貴,但我們手上早有可以當 iBeacon 的玩意呀。開發 iOS App 的我們,大部分都有一台 iPhone & 一台 Mac,只要不是太舊的 iPhone & Mac,其實都有變身成 iBeacon 的藍芽(bluetooth)能力。
將 Mac 或 iPhone 變成 iBeacon
我們可以參考 Apple 官方文件撰寫程式,將 iPhone 變成 iBeacon。
不過這樣的程式早有人幫我們寫好,用不著自己寫。有需要的朋友可參考以下連結下載別人寫好的 Mac App,將 Mac 變成 iBeacon。
啟動 App 後,將看到以下的畫面,我們可調整 iBeacon 的 UUID,Major,Minor & Power。
每個 iBeacon 裝置都有 UUID(128 bit),major(16 bit) & minor(16 bit),因此我們可透過這三個欄位區分不同的 iBeacon。依不同的應用,我們有以下三種設計和辨識 iBeacon 的方法:
- 讓 iBeacon 裝置使用同樣的 UUID & major,利用 minor 區分。
- 讓 iBeacon 裝置使用同樣的 UUID,利用 major & minor 區分。
- 利用 UUID ,major ,minor 區分。
如果不是太複雜的應用,一般用方法一或方法二已足夠。比方博物館裡有十幅畫作,我們可在每幅畫作身上配置 iBeacon,給每個 iBeacon 同樣的 UUID & major,搭配不同的 minor 來區分。
若想修改 UUID,但不知要輸入什麼,可參考以下方法產生 UUID。
Major & Minor 可輸入數字,但記住它只有 16 bit,因此最大只能輸入 65535。
確定 UUID,Major & Minor 後,點選 Start 按鈕,即可讓 Mac 成為 iBeacon,開始發射(broadcast)訊號。
ps: 我們也可以透過 App 讓 iPhone 或 iPad 成為 iBeacon,比方使用 BLE Scanner。
開發偵測 iBeacon 的 iOS App
接下來彼得潘將一步步說明開發 iBeacon iOS App 的步驟,有興趣的朋友也可進一步參考 Apple 官方的說明。
1 加入存取位置的權限文字說明。
依據 Apple 以下表格的說明,為了偵測 iPhone 進入某個 iBeacon 範圍(Raging monitoring) & 量測 iPhone & iBeacon 間的距離 (iBeacon ranging),我們需在 Info 頁面加入以下兩種權限的存取文字說明,Privacy — Location Always and When In Use Usage Description & Privacy — Location When In Use Usage Description。
2 建立 CLLocationManager,將 controller 設為 CLLocationManager 物件的 delegate & 要求存取位置的權限。
import CoreLocation
class ViewController: UIViewController { var locationManager: CLLocationManager! override func viewDidLoad() { super.viewDidLoad() locationManager = CLLocationManager() locationManager.delegate = self locationManager.requestAlwaysAuthorization()}extension ViewController: CLLocationManagerDelegate {}
3 開始偵側 iBeacon。
func monitorBeacons() { if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) { let proximityUUID = UUID(uuidString: "B0702880-A295-A8AB-F734-031A98A512DE") let beaconId = "deeplove" let region = CLBeaconRegion(proximityUUID: proximityUUID!, identifier: beaconId) locationManager.startMonitoring(for: region) }}override func viewDidLoad() { super.viewDidLoad() locationManager = CLLocationManager() locationManager.delegate = self locationManager.requestAlwaysAuthorization() monitorBeacons()}
說明:
(1) 利用 CLBeaconRegion 指定想要偵測的 beacon。
let region = CLBeaconRegion(proximityUUID: proximityUUID!, identifier: beaconId)
產生 CLBeaconRegion 的方法有三種,我們可視需求選擇不同的方法。
public init(proximityUUID: UUID, identifier: String)public init(proximityUUID: UUID, major: CLBeaconMajorValue, identifier: String)public init(proximityUUID: UUID, major: CLBeaconMajorValue, minor: CLBeaconMinorValue, identifier: String)
在剛剛的例子裡,我們假設目前創業為艱,只有錢買一個 iBeacon,因此利用 UUID 辨識足已,不需要 major & minor。(ps: 請記得 UUID 要和剛剛在 Mac iBeacon App 上設定的 UUID 一致,如此才能偵測到化身 iBeacon 的 Mac。)
let region = CLBeaconRegion(proximityUUID: proximityUUID!, identifier: beaconId)
(2) 開始偵測是否進入 / 離開 iBeacon 的範圍。
locationManager.startMonitoring(for: region)
4. 定義 CLLocationManagerDelegate 的 function,偵測進入 / 離開 beacon 的範圍。
extension ViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { print("enter region") } func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { print("exit region") }}
5 量測 iPhone 跟 beacon 之間的距離
有時我們還想進一步知道 iPhone & iBeacon 之間的距離。如下圖所示,Apple 將距離分成四種等級,由近到遠依序為 Immediate(非常近),Near,Far & Unknown。
比方在看博物館展覽時,我們希望走到畫作前時 App 才呈現相關的資訊,於是我們可從程式判斷距離為 Immediate 時顯示畫作資訊,如此才不會距離畫作還有一大段距離,連畫都看不清楚時就奇怪地顯示畫作資訊。
extension ViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { print("enter region") if CLLocationManager.isRangingAvailable() { locationManager.startRangingBeacons(in: region as! CLBeaconRegion) } }
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { print("exit region") locationManager.stopRangingBeacons(in: region as! CLBeaconRegion) }}
說明:
利用 startRangingBeacons & stopRangingBeacons 開始 / 停止偵測 iPhone & beacon 的距離。我們在 locationManager(_:didEnterRegion:) 裡才開始偵測距離,因為偵測距離比較耗電,它將不斷地量測更新距離。
不過有一點值得注意的,實測時發現 locationManager(_:didEnterRegion:) 有可能沒被觸發,進而讓程式不會呼叫 startRangingBeacons,因此永遠不會偵測到 iPhone 已經在 beacon 旁邊。比方若你原本就在 beacon 範圍裡,locationManager(_:didEnterRegion:) 將不會被觸發。因此測試時最好一開始遠離 beacon,在 beacon 範圍之外,然後再慢慢地靠近 beacon,進入 beacon 的範圍,這樣較容易觸發 locationManager(_:didEnterRegion:)。(ps: 若想確保我們一定能偵測到 iPhone 就在 beacon 旁邊,另一種做法是一開始就同時呼叫 startMonitoring & startRangingBeacons)
let region = CLBeaconRegion(proximityUUID: proximityUUID!, identifier: beaconId)locationManager.startMonitoring(for: region)locationManager.startRangingBeacons(in: region)
6 定義 CLLocationManagerDelegate 的 function 量測距離。
從 CLBeacon 物件的 proximity 判斷距離。當我們在 beacon 附近時,此 function 將不斷被呼叫,告訴我們最新的距離資訊。
func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { if beacons.count > 0 { let nearestBeacon = beacons[0] print(nearestBeacon.proximityUUID, nearestBeacon.major, nearestBeacon.minor) switch nearestBeacon.proximity { case .immediate: print("immediate")
case .near: print("near") case .far: print("far") case .unknown: print("unknown") @unknown default: print("@unknown default") } }}
實驗:
利用 Mac 當 iBeacon,然後啟動剛剛開發的程式偵測 beacon,我們可利用此難得的機會運動一下,走走路讓 iPhone 靠近和遠離 Mac,觀察 locationManager(_:didRangeBeacons:in:) 裡 CLBeacon 距離的變化。
在背景偵測 beacon & 顯示通知
我們也可以在背景偵測 beacon,甚至在 App 沒啟動但進入 beacon 範圍時,由 iOS 自動啟動 App,然後觸發 locationManager:didEnterRegion:。比方以下例子,我們在 locationManager:didEnterRegion: 觸發時顯示通知訊息:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { let content = UNMutableNotificationContent() content.title = "注意" content.subtitle = "小明就在你身邊" content.badge = 1 content.sound = UNNotificationSound.default let request = UNNotificationRequest(identifier: "notification", content: content, trigger: nil) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) if CLLocationManager.isRangingAvailable() { locationManager.startRangingBeacons(in: region as! CLBeaconRegion) }}
為了顯示通知,記得要先徵求使用者的同意。
import UIKitimport UserNotifications@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow?func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { granted, error in if granted { } else { print("使用者不同意,不喜歡米花兒,哭哭!") } }) return true}
實驗步驟:
- 安裝 App,然後啟動 App 同意權限後,先將它殺掉,維持 App 不啟動的狀態。
- 把 iPhone 收進胸前左邊口袋,走到遠離 beacon 的天涯海角散心。
- 從天涯海角出發,朝著心愛的 beacon 方向前進。
一旦進入 beacon 的訊號範圍,此時將觸發 function locationManager:didEnterRegion:,口袋裡的 iPhone 將傳來通知聲音,顯示著通知訊息,小明就在你身邊
。
ps:
- 若你原本就在 beacon 範圍裡,locationManager(_:didEnterRegion:) 將不會被觸發。因此測試時請記得先遠離 beacon。
2. 根據 Apple 文件的說明,為了在背景偵測到進入 beacon 範圍時啟動 App,我們得將 Capabilities 頁面 Background Modes 的 Location Updates 打開。不過根據實測的結果,不需打開 Location Updates 一樣可觸發 locationManager:didEnterRegion: & application(_:didFinishLaunchingWithOptions:)。