iOS蓝牙开发之iBeacon篇(二)


那一天我二十一岁,在我一生的黄金时代。我有好多奢望。我想爱,想吃,还想再一瞬间变成天上半明半暗的云。我来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。
——王小波《黄金时代》

前言

照例,如果只是需要监听iBeacon的demo代码,可以直接点击下载,不需要往下看了😂

如果想了解iBeacon相关信息,请参考iOS蓝牙开发之iBeacon篇(一)

  • 虽然iBeacon也是基于Ble的,从硬件上看也是一套东西,但是Apple公司把他封装成了两套东西,所以在iOS移动端开发来说,这是两个完全不同的东西,调用完全不同框架的api.
  • Apple公司为了省电和隐私限制只能监听指定UUID的iBeacon,无法像安卓一样搜索附近所有的iBeacon信息,虽然iBeacon扫描底层可以获取到附近的所有的iBeacon信息(底层就是获取附近所有的iBeacon然后和当前监听的iBeacon标识对比,然后回调给上层),如果你只是想在你自己的手机上测试下,可以参考AnyiBeacon-iOS这个项目,但是只能作为测试使用,因为涉及到Apple的私有api,是不允许上架App Store的。
  • 想了解iBeacon底层数据协议格式的,可以参考What is the iBeacon Bluetooth Profile 这篇文章。

初始化

  • 在 iOS8.0之后的时候如果想使用iBeacon,必须让用户授权
    • 在info.plist文件里面配置下面的key
1
2
3
NSLocationAlwaysAndWhenInUseUsageDescription // 推荐
NSLocationWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
  • 在capabilities里面开启Background Modes的 Location updates
  • 由于iBeacon是基于CoreLocation框架的,所以先导入头文件并实例化位置管理者
1
2
import CoreLocation
let locationManager = CLLocationManager()
  • 请求授权并设置代理
1
2
3
4
5
6
7
8
override func viewDidLoad()
{
super.viewDidLoad()

// Do any additional setup after loading the view.
locationManager.requestAlwaysAuthorization() // 必须要申请权限,否者不会回调扫描到beacons的代理方法
locationManager.delegate = self
}

添加需要监听的iBeacon

  • 这个是添加iBeacon信息界面代理回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MARK: AddBeacon
extension ItemsViewController: AddBeacon {
func addBeacon(item: Item)
{
items.append(item)

tableView.beginUpdates()
let newIndexPath = IndexPath(row: items.count - 1, section: 0)
tableView.insertRows(at: [newIndexPath], with: .automatic)
tableView.endUpdates()

startMonitoring(item) // 开始监控
persistItems() // 持久化到本地
}
}
  • Item为一个iBeacon信息的数据模型, 即包含如下信息
1
2
3
4
5
let name: String // 非必须
let icon: Int // 非必须
let uuid: UUID
let majorValue: CLBeaconMajorValue // 非必须
let minorValue: CLBeaconMinorValue // 非必须

开始监听和停止监听

  • 根据一个iBeacon的参数(iBeacon硬件提供或者自己用iPhone或者mac模拟)初始化一个CLBeaconRegion
  • 两种监听模式
1
2
3
4
5
6
7
8
9
10
11
12
func startMonitoring(_ item: Item)
{
let region = item.asBeaconRegion()
locationManager.startMonitoring(for: region) // 低功耗,支持后台监听
locationManager.startRangingBeacons(in: region) // 大功耗,不支持后台监听
}

// MARK: 根据模型数据初始化一个Beacon Region
func asBeaconRegion() -> CLBeaconRegion
{
return CLBeaconRegion(proximityUUID: uuid, major: majorValue, minor: minorValue, identifier: name)
}
  • 如果想停止监听某个iBeacon
1
2
3
4
5
6
func stopMonitoring(_ item: Item)
{
let region = item.asBeaconRegion()
locationManager.stopMonitoring(for: region)
locationManager.stopRangingBeacons(in: region)
}

监听到iBeacon信息回调

  • 开始监听某个Beacon
1
2
3
4
func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion)
{
print("didStartMonitoringFor\(region)")
}
  • 监听到Beacons
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// MARK: CLLocationManagerDelegate
extension ItemsViewController: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion)
{
print("didRangeBeacons \(beacons.count)个")
var indexPaths = [IndexPath]()
for beacon in beacons {
for row in 0..<items.count {
// 检测是否是需要监控的beacon
if items[row] == beacon {
items[row].beacon = beacon
indexPaths += [IndexPath(row: row, section: 0)]
}
}
}

if let visiblaRows = tableView.indexPathsForVisibleRows {
let rowsToUpdate = visiblaRows.filter{ indexPaths.contains($0) }
for row in rowsToUpdate { // 刷新可见cell上对应的Beacon距离信息
let cell = tableView.cellForRow(at: row) as? ItemCell
cell?.refreshLocation()
}
}
}
  • 其他的回调方法
  • 注意:

    • 如果需要监听进入某个区域或者从某个区域离开,则需要在初始化Beacon Region的时候订阅
    • 不然进入和离开区域不回回调

      1
      2
      > region.notifyOnEntry = YES;  
      > region.notifyOnExit = YES;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// monitoring 监听某个beacon失败    
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error)
{
print("Failed monitoring region\(error.localizedDescription)")
}
// 位置管理者出现错误信息
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
print("Location manager failed \(error.localizedDescription)")
}
// ranging 监听某个beacon失败
func locationManager(_ manager: CLLocationManager, rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error)
{
print("rangingBeaconsDidFailFor")
}
// 已经进入到某个区域
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
print("didEnterRegion")
}
// 已经从某个区域离开
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("didExitRegion")
}

关于推送

  • app现在能显示iBeacon设备,并且还能实时监控它们的距离。但是当app没有处于运行状态时,如iBeacon设备在宠物猫脖子上但是宠物猫跑丢了!
  • 此时app就需要在猫离开区域的时候通知用户。
  • 在AppDelegate导入通知框架并初始化一个位置管理者
1
2
3
4
import CoreLocation
import UserNotifications

var locationManager = CLLocationManager()
  • 程序启动的时候,配置位置监听和设置通知
1
2
3
4
5
6
7
8
9
10
11
12
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.

// 注册通知,请求权限
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in }

// 设置代理
locationManager.delegate = self

return true
}
  • 监听目标离开区域并发送本地通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension AppDelegate: CLLocationManagerDelegate
{
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
guard region is CLBeaconRegion else {
return
}
let content = UNMutableNotificationContent()
content.title = "Forget Me Not"
content.body = "Are you forgetting something?"
content.sound = .default()

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

参考

官方文档
维基百科
iOS 中 iBeacon 开发
iBeacon Tutorial with iOS and Swift
How to detect ibeacon device without knowing UUID in iOS?
Detecting beacons via iBeacon Monitoring & Ranging vs CoreBluetooth scanForPeripheralsWithServices

要不要鼓励一下😘