本文由CocoaChina譯者培子翻譯自Raywenderlich
原文:iBeacons Tutorial with iOS and Swift
備注:該教程針是Adrian Strahan針對iOS 8,swift1.2,以及Xcode6.3而更新的。原始文章由Tutorial Team成員Chris Wagner供稿。
你曾經想過用手機在一個大型建築物中為自己定位嗎,比如購物中心,或者棒球場。
當然,GPS可以讓你得知自己身處哪一座建築物裡。但是如果想要在這些鋼筋混凝土堆砌而成的建築中獲得精確的GPS信號,只能祝你好運了。你所需要的是內置在建築物中一些設備,(通過它們)讓手機獲取確定你的位置。
iBeacon,我們來了!在這篇iBeacons教程中,將會開發一個App,功能包括,紀錄已知的iBeacon發射器;還有當你的手機設備移出它的信號輻射范圍時,App會提醒你。這個App的一大用處是:將iBeacon發射器放置在你的電腦包、錢包裡,甚至綁在貓咪的項圈上,以及其他貴重物品上等。一旦設備移出iBeacon輻射范圍,App就會檢測到並且通知你。
如果想繼續深入,你需要一台iOS設備和一個iBeacon設備。如果沒有iBeacon設備,但還有另外一個iOS設備,你也可以把它當作iBeacon設備來用。
開始吧
目前有很多可用的iBeacon設備,在Google一搜一大堆。但是蘋果公司引進iBeacon時,他們還聲稱任何兼容iOS的設備都能充當iBeacon。目前包括以下設備:
iPhone4s 或者 之後的iPhone設備
第三代iPad或者 之後iPad設備
iPad mini 或者 之後iPad mini設備
第五代iPod touch 或者之後iPod touch設備
備注:如果沒有單獨的iBeacon發射器,但擁有一台支持iBeacons的iOS設備,你可以對照《what's new in core location of iOS 7 by Tutorials》書中第22章的描述,開發一個充當iBeacon角色的App。
iBeacon就是一種低功耗藍牙設備,它能以特定的數據結構廣播數據信息。這些屬性雖然超出了本篇教程的討論范圍,但是對理解iOS能夠監測iBeacon設備很重要,這些設備廣播三個數據:UUID,major和minor。UUID是universally unique identifier的首字母縮寫,它是一個128位的值,通常以十六進制的方式顯示:B558CBDA-4472-4211-A350-FF1196FFE8C8
在iBeacon的環境下,UUID用來表示設備的最高級別標識。
Major和minor值則是在UUID下提供更細微的辨別標識。它們的值用16位無符號整型數簡單表示,用來識別單個iBeacon設備,即使這些設備有著相同的UUID。
例如,你有多家百貨公司,打算讓所有的iBeacon設備發射相同的UUID信息,但是每家百貨公司擁有各自的major值,並且在每家百貨公司的各部門又擁有各自的minor值。這樣App就可以對放置在邁阿密的弗羅裡達分店的鞋業部門的iBeacon設備做相應的信息反饋。
ForgetMeNot 開始項目
從這兒下載工程啟動文件,它的界面很簡單,就是在表格視圖中添加和刪除對象。表格裡的每一個對象代表一個iBeacon設備,在現實生活中,它可以代表你不想遺失的東西。
啟動並構建App,你會看到一個空的列表,沒有任何對象。點擊+按鈕,為它添加新項,如下:
首屏
添加新項,你需要為新對象命名,還有與之對應的值。可以通過查看iBeacon的文檔來獲得它的UUID。立即添加進去吧,或者用一些占位符值,如下:
添加項目
點擊save按鈕,返回到列表界面,就會看到location顯示為Unknown的對象,如下:
添加的項目列表
想添加多少,就添加多少,或者刪掉已有的。NSUserDefaults會保存列表裡的選項,以便再次打開App時可用。
界面上,看上去也就那麼回事;最有趣的部分藏在了表象之下。該App的獨特之處就在於表格裡顯示的Item類。
在Xcode中打開Item.swift。該類對應著界面從用戶那裡請求的信息,它遵循NSCoding協議,因此可以被串行和並行的存儲到硬盤上。
現在看一下AddItemViewController.swift。這是用來添加新Item對象的視圖控制器。除了對用戶輸入做了些驗證,確保用戶輸入有效的名稱和UUID之外,它就是一個簡單的UITableViewController。
一旦nameTextField和uuidTextField內容有效,右上角的Save按鈕就會變為可點擊狀態了。
既然已經熟悉了項目啟動文件,你就能夠在你的工程中實現iBeacon了。
Core Location 許可
你的設備當然不會自動監測iBeacon的,所以首先你得告知它。CLBeaconRegion類代表一個iBeacon;CL前綴的類表示它屬於Core Location框架。
iBeacon與Core Location關聯在一起看上去有點奇怪,因為它就是一個藍牙設備而已,但是也可以這麼認為,那就是iBeacon提供小范圍定位功能,而GPS提供的是大范圍定位功能。當想讓iOS設備充當iBeacon時,你還需要引入Core Bluetooth框架,但只想檢測iBeacon設備,你只需要Core Location就行了。
首先為Item引入CLBeaconRegion。
打開Item.swift,在頂部添加如下代碼:
import CoreLocation
接下來,更新majorValue和minorValue定義,並初始化如下:
let majorValue: CLBeaconMajorValue let minorValue: CLBeaconMinorValue init(name: String, uuid: NSUUID, majorValue: CLBeaconMajorValue, minorValue: CLBeaconMinorValue) { self.name = name self.uuid = uuid self.majorValue = majorValue self.minorValue = minorValue }
CLBeaconMajorValue?和?CLBeaconMinorValue都是UInt16型,用來表示major和minor值。
雖然它們的數據類型一樣,但是為了提高Item的可讀性和增加數據的安全性,你最好不要把major和minor值搞混。
打開ItemsViewController.swift,在頂部引入Core Location:
import CoreLocation
為其添加如下屬性:
let locationManager = CLLocationManager()
當引入Core Location功能時,需要用到這個CLLocationManager對象。
然後,更新viewDidLoad(),如下:
override func viewDidLoad() { super.viewDidLoad() locationManager.requestAlwaysAuthorization() loadItems() }
如果設備沒有給App授權,它就會調用requestAlwaysAuthorization()方法來提示用戶是否允許使用定位服務。在iOS8中,Always和When in Use是最新有關定位授權的狀態。在App使用Always權限授權時,只要app在前台或者後台處於運行狀態,它就可以啟動所有可用的定位服務。
因為該教程對iBeacon一直進行區域監測,所以當app處於前台或者後台運行時,需要Always定位許可來觸發區域事件。
iOS8要求你在Info.plist?中設置一串字符,該字符串會在app請求定位服務時顯示出來。如果不設置它,定位服務就會無效,甚至都得不到任何警告!
打開Info.plist,選中Information Property List 後,點擊+,添加新的一行。
遺憾的是,需要添加的key不是在下拉列表中預定義好的,需要自己輸入進去。把key設置為NSLocationAlwaysUsageDescription,Type設為String類型。然後輸入提示文字,告訴用戶用戶為何要開啟定位服務,例如:"ForgetMeNot would like to teach you how to use iBeacons!"
啟動並構建app,一旦運行,你會看到一段消息,詢問你是否允許app使用定位服務:
允許訪問位置
選擇Allow,app就能夠追蹤iBeacon設備了
監聽iBeacon
現在app有了定位服務的授權,是時候搜索beacon設備了!在ItemsViewController.swift的底部添加類extension,如下:
// MARK: - CLLocationManagerDelegate extension ItemsViewController: CLLocationManagerDelegate { }
這段代碼表示ItemsViewController遵循CLLocationManagerDelegate協議。接著在extension裡添加委托方法,讓它們結合在一起。
在viewDidLoad方法最後一行添加如下代碼:
locationManager.delegate = self
設置CLLocationManager的委托對象為self,以便接收委托方法的回調。
有了CLLocationManager對象,你可以指導app使用CLBeaconRegion對一些指定的區域開啟監測。當注冊了一個監控區域,之後只要啟動app,這些區域就會存在下去。當你對一個交叉區域的邊界作出反應,而app沒有運行時,這點很重要。
列表中的iBeacon對象由Item類的items數組屬性表示。然而CLLocationManager希望你提供一個CLBeaconRegion對象來開啟監控。
在ItemsViewController.swift中創建如下輔助方法:
func beaconRegionWithItem(item:Item) -> CLBeaconRegion { let beaconRegion = CLBeaconRegion(proximityUUID: item.uuid, major: item.majorValue, minor: item.minorValue, identifier: item.name) return beaconRegion }
該方法通過提供的Item,返回一個CLBeaconRegion對象。
可以看出CLLBeaconRegion和Item之間有相似的數據結構,所以生成CLBeaconRegion對象很簡單,因為它有直接對應的屬性UUID,major值和minor值
現在創建一個方法來監控已有的Item對象,給ItemsViewController添加如下代碼:
func startMonitoringItem(item: Item) { let beaconRegion = beaconRegionWithItem(item) locationManager.startMonitoringForRegion(beaconRegion) locationManager.startRangingBeaconsInRegion(beaconRegion) }
該方法使用一個Item參數,調用之前定義的方法生成CLBeaconRegion。然後讓location manager開始監控已有的區域,並在該區域內檢索iBeacon設備。
在給定的區域內,檢索就是發現iBeacon設備的過程,並確定iBeacon設備與iOS設備之間的距離。一台接收到iBeacon發射信息的iOS設備能估算出它與iBeacon之間的距離。這個距離呗劃分為三個區域范圍:
Immediate 幾厘米之內
Near 幾米之內
Far 10米開外
備注:Far,Near和Immediate對應的實際距離不是固定的,在Stack Overflow Question有提到它,並給它一個組略的距離范圍。
默認來講,無論app是否運行,只要你進入或者走出監控區域,app都會通知你。另一方面,檢索iBeacon設備這一過程只會在app處於運行狀態時才會監測出區域距離。
還需要在某個Item區域被刪除時,終止對它的監控。在ItemViewController添加如下代碼:
func stopMonitoringItem(item: Item) { let beaconRegion = beaconRegionWithItem(item) locationManager.stopMonitoringForRegion(beaconRegion) locationManager.stopRangingBeaconsInRegion(beaconRegion) }
上述方法的作用剛好與startMonitoringItem方法相反,直到CLLocationManager終止監控和檢索活動。
現在,已經創建了開始和終止方法,是時候用它們了!開啟監控的正確時機是在用戶為列表添加新的item對象的時候。
看一下ItemsViewController中的saveItem(_:),該unwind segue轉場是在用戶點擊AddItemViewController的Save按鈕時觸發,它同時生成了一個監控區域。在這個方法中找到調用persistItems()的那行,在它之前的一行添加如下代碼:
startMonitoringItem(newItem)
當用戶保存一個item對象時,就會激活這個監控。同樣的,當啟動app時,app從NSUserDefaults加載已存的item對象,這也就意味著啟動的同時就需要開啟對應的區域監控。
在ItemsViewController .swift中,找到loadItems(),在for循環中添加如下代碼:
startMonitoringItem(item)
這會確保每個item區域都處於受監控狀態。
還有,你需要關注一下從列表中刪除item。找到tableView(_:commitEditingStyle:forRowAtIndexPath:),在itemToRemove之後添加如下代碼:
stopMonitoringItem(itemToRemove)
當用戶刪除表視圖某行時,就會調用這個委托方法。現有的代碼處理的是將該行對象從數據模型和視圖上刪除,剛剛加的代碼將會終止對item監控。
此時此刻,你已經完成了很多事情!app已經有開啟和終止對制定iBeacon的監控。
這個階段,可以啟動並構建app了;但是盡管已經注冊的iBeacon在你的app檢索范圍內,可是目前app無法對發現iBeacon設備作出任何反饋......還需要繼續完善它!
發現iBeacon的反饋
既然location manager已經監控iBeacon,是時候通過CLLocationManagerDelegate的某些方法對iBeacon作出反應。
首先也是最重要的是錯誤處理,因為你正在處理設備指定的硬件信息,想知道監控或者檢索失敗的任何可能原因。
給ItemsViewController.swift裡的CLLocationManagerDelegate的類extension添加下面兩個方法:
func locationManager(manager: CLLocationManager!, monitoringDidFailForRegion region: CLRegion!, withError error: NSError!) { println("Failed monitoring region: \(error.description)") } func locationManager(manager: CLLocationManager!, didFailWithError error: NSError!) { println("Location manager failed: \(error.description)") }
這些方法對監控iBeacon時接收的所有錯誤信息做一個簡單記錄。
如果app一切運行正常,你不會看到這些方法的輸出信息。然而,如果某些地方出了問題,很有可能從錯誤日志中獲得很有價值的信息。
下一步就是實時顯示iBeacon與iOS設備之間的距離。在CLLocationManagerDelegate的類extension裡,實現如下委托方法:
func locationManager(manager: CLLocationManager!, didRangeBeacons beacons: [AnyObject]!, inRegion region: CLBeaconRegion!) { if let beacons = beacons as? [CLBeacon] { for beacon in beacons { for item in items { // TODO: Determine if item is equal to ranged beacon } } } }
這個委托方法會在iBeacon進入監控范圍,或者移除監控范圍,或者iBeacon輻射范圍發生改變的時候被調用。
該app的目的就是使用由上述委托方法提供的iBeacon數組來更新列表的item對象,並顯示它們與設備的感應距離。重復迭代beacons數組,之後再迭代items數組,查看是兩者之間是否有匹配的部分。稍後來處理TODO部分代碼。
打開item .swift,給Item類添加如下屬性:
dynamic var lastSeenBeacon: CLBeacon
該屬性存儲的是最後一個與之匹配的CLBeacon對象,用來顯示距離信息。它有一個dynamic修飾符,以便於稍後對它使用key-value observation。
在Item.swift底部,在類定義的外面,添加==操作符判斷如下代碼:
func ==(item: Item, beacon: CLBeacon) -> Bool { return ((beacon.proximityUUID.UUIDString == item.uuid.UUIDString) && (Int(beacon.major) == Int(item.majorValue)) && (Int(beacon.minor) == Int(item.minorValue))) }
該等號操作符函數比較CLBeacon與Item對象,檢查它們是否相等--即,它們所有的標識是否匹配。這種情形下,如果UUID,major和minor值全部相同,那麼CLBeacon與Item對象相等。
現在繼續完成檢索的委托方法,調用上述輔助方法。打開ItemsViewController.swift,回到locationManager(_:didRangeBeacons:inRegion:)。替換for循環裡的TODO部分,如下:
if item == beacon { item.lastSeenBeacon = beacon }
這裡,當發現一個item與iBeacon匹配時,把它賦值給lastSeenBeacon。你會發現item和iBeacon受益於之前等號操作符函數!
是時候使用該屬性來顯示監測到的iBeacon設備與iOS設備之間的距離。
打開ItemCell.swift,在didSet屬性觀察者起始部位,添加如下代碼:
item?.addObserver(self, forKeyPath: "lastSeenBeacon", options: .New, context: nil)
當為cell設置item時,同樣要為lastSeenBeacon添加一個觀察者。為了保持平衡,還要在cell已經設置過了item時,刪除該觀察者。為didSet添加一個willSet屬性觀察者。確保它屬於item屬性:
willSet { if let thisItem = item { thisItem.removeObserver(self, forKeyPath: "lastSeenBeacon") } }
這會確保只有一個item 對象被觀察。
當然,當cell被廢棄時,同樣需要刪除觀察者。還在ItemCell.swift,添加如下代碼:
deinit { item?.removeObserver(self, forKeyPath: "lastSeenBeacon") }
既然正在觀測距離的變化,你就可以在iBeacon的距離發生變化的時候通過一些邏輯規則來作出反饋。
每個CLBeacon對象都有一個proximity屬性,它是一個包含Far,Near,Immediate和Unknown的枚舉。
在ItemCell.swift中,為Core Location添加導入的申明:
import?CoreLocation
下一步,為ItemCell添加如下代碼:
func nameForProximity(proximity: CLProximity) -> String { switch proximity { case .Unknown: return "Unknown" case .Immediate: return "Immediate" case .Near: return "Near" case .Far: return "Far" } }
該發放返回一個易讀的遠近值,後面會用到它。
接著,添加如下代碼:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) { if let anItem = object as? Item where anItem == item && keyPath == "lastSeenBeacon" { let proximity = nameForProximity(anItem.lastSeenBeacon!.proximity) let accuracy = String(format: "%.2f", anItem.lastSeenBeacon!.accuracy) detailTextLabel!.text = "Location: \(proximity) (approx. \(accuracy)m)" } }
每次lastSeenBeacon發生改變時,都會調用這個方法,它會用CLBeacon的proximity值和accuracy值設置cell的detailTextLabel.text屬性。
後面這個accuracy的值即使你的iOS設備和iBeacon沒有移動,由於無線電頻率的緣故,也會一直浮動,因此不要指望它來達到beacon的精確定位。
現在確保已經注冊了iBeacon,然後讓你的iOS設備逐漸靠近它,或者遠離它。當你移動時,你會看到標簽也會隨之更新,如下:
你會發現proximity和accuracy值受到iBeacon位置的影響比較劇烈;如果把它放在類似箱子,包之類的東西裡,信號就會受到阻礙,這是因為iBeacon是低功耗設備,它的信號很容易被減弱。
記住這點,在設計app時,需要把iBeacon放置在最妥善的位置上。
推送
app看上去已經很棒了;能顯示iBeacon設備,並且還能實時監控它們的距離。但是這還不是app的終極目標。當app沒有處於運行狀態時,用戶忽略了他們的手提包,或者寵物貓跑丟了--更有甚者,貓和手提包都不翼而飛了!
他們是不是好可憐?
此刻,你可能注意到為app添加iBeacon功能不需要太多代碼。當貓貓和手提包都不見了時,添加一個推送也一樣簡單!
打開AppDelegate .swift,導入CoreLocation,如下:
import CoreLocation
接著,讓AppDelegate遵循CLLocationManagerDelegate協議,在AppDelegate .swift底部添加如下代碼(在類結束符下面)
// MARK: - CLLocationManagerDelegate extension AppDelegate: CLLocationManagerDelegate { }
在這之前,你需要初始化location manager,設置它的delegate。
給AppDelegate添加一個locationManager屬性,用CLLocationManager對象實例化它:
let locationManager=CL LocationManager()
然後在application(_:didFinishLaunchingWithOptions:):添加如下代碼:
locationManager.delegate=self
要知道app中所有的location manager都能通過startMonitoringForRegion(_:)共同監控你添加的區域(location manager是單例)。因此最後一步,只需要在走出某個區域時,對Core Location何時喚醒app作出反應就行了。
在AppDelegate.swift底部的類extension裡添加如下代碼:
func locationManager(manager: CLLocationManager!, didExitRegion region: CLRegion!) { if let beaconRegion = region as? CLBeaconRegion { var notification = UILocalNotification() notification.alertBody = "Are you forgetting something?" notification.soundName = "Default" UIApplication.sharedApplication().presentLocalNotificationNow(notification) } }
當你走出某個區域,location manager就會調用上述方法,這是app一大亮點。假如離手提包越來越近,app就不需要提醒你,只有離它太遠才會觸發。
首先需要確定區域是否是CLBeaconRegion,因為當執行地理區域監控時,它還有可能是CLCircularRegion。然後用"Are you forgetting something?"消息發一個本地推送。
在iOS 8之後,app使用本地推送或者遠程推送,必須注冊推送的類型。系統給用戶權限來限制不同類型推送的界面顯示。假如app不能使用這些推送類型,即使它們是在推送載荷被指定過,系統也不會對app icon標記,不會顯示提示信息,或者沒有提示音效。
在application(:_didFinishLaunchingWithOptions:):的最上面添加如下代碼:
let notificationType:UIUserNotificationType = UIUserNotificationType.Sound | UIUserNotificationType.Alert let notificationSettings = UIUserNotificationSettings(forTypes: notificationType, categories: nil) UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
這段代碼就是當app收到一個推送時,就會顯示一段提示信息,並且播放一段音效。
構建項目;確保app能監測到至少一個iBeacon設備,點擊Home按鈕讓app進入後台模式--這是現實生活中的場景,當你在處理其他事情的時候,比如處理Ray Wenderlich的另一個app時,希望這個後台app能通知你。現在遠離iBeacon,一旦離得足夠遠,有就會收到這個推送,如下:
鎖屏上的通知
備注:蘋果系統以未公開的方式延遲退出推送。這樣設計可能方便當你在區域范圍的邊緣游蕩或者iBeacon信號被干擾時,app不會接收之前的推送。以筆者的經驗來說,在iBeacon離開區域范圍一分鐘時推送就會退出。
更進一步?
還沒有為你的代碼綁定iBeacon嗎?從這兒下載最終的項目(here),教程所說的全在這裡。
你已經有了一款很有用的app,來監控哪些比較難追蹤的東西。加一些額外的思考和編程功底,你還可以給app添加更多有用的功能:
通知用戶那個iBeacon移出了監控范圍
重復推送,確保用戶能看到它
提醒用戶iBeacon何時又返回監控范圍
這篇iBeacons教程僅僅只是揭開iBeacon所有功能的冰山一角。
iBeacon不局限於傳統app;你還可以在Passbook中使用它。比如,當你去看電影時,可以提供Passbook通行證當作電影票。當顧客走到附近有iBeacon設備的檢票員面前時,app自動在iPhone上顯示電影票!
對本篇教程有任何疑問或者意見,或者你有與iBeacon相關的好點子,歡迎加入我們的討論!