本文由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相关的好点子,欢迎加入我们的讨论