iOS开发Swift

Swift iBeacon 开发

iBeacon是什么?

维基百科:iBeacon是苹果公司提出的”一种可以让附近手持电子设备检测到的一种新的低功耗、低成本信号传送器”的一套可用于室内定位系统的协议。这种技术可以使一个智能手机或其他装置在一个iBeacon基站的感应范围内执行相应的命令。

百度百科:iBeacon是苹果公司2013年9月发布的移动设备OS(iOS7)上配备的新功能。其工作方式是,配备有低功耗蓝牙(BLE)通信功能的设备使用BLE技术向周围发送自己特有的ID,接收到该ID的应用软件会根据该ID采取一些行动。

使用场景:
  • 推送感知场景的信息

当用户移动到他们感兴趣的区域时,iBeacons 可以用来给他们推送信息。博物馆就是一个典型的例子,设想在每一个展品的位置放置一个 iBeacons 设备,当用户走近展品的时候,手机应用自动展示看到展品的更多信息会有多棒! 这就需要手机应用侦听发射器来了解有哪些发射器接近哪些展品,通过这样的匹配,发射器将定位用户在博物馆中的位置并让应用做出合理的响应。

  • 定位追踪

作为推送感知场景的信息概念的扩展,你也可以使用 iBeacons 作为一种追踪用户的方式。设想在一个博物馆或者杂货店的建筑中遍布 iBeacons ,随着用户的移动,由他们通过发射器的顺序,你可以侦测出他们的移动轨迹。这允许你追踪用户的行踪,并且总结出最普遍的行进路线和行进模式。

从开发者角度的思考:

iBeacon向四面八方不停地广播信号,就像是往平静的水面上扔了一块石子,泛起层层涟漪(俗称水波),波峰相当于iBeacon的RSSI(接受信号强度指示),越靠近中心点的地方波峰越高(RSSI越大),这个波峰的大小(RSSI的值)受到扔石子时用力大小(发射功率)和水质(周围环境因子)的影响,离中心点越远水波越趋向于平静,超过了一定值,水波会消失于无形,也就是说iBeacon向外广播的距离是有范围的,超过了这个范围,将接受不到iBeacon的信号。

总体来看,iBeacon中有两个角色:发射者 :各个硬件设备;接受者:智能终端(手机),发射者通过BLE 的广告通信通道,以一定时间间隔向外广播数据包(一般是每秒两三次),接收者可以通过终端提供的功能来接收,达到信息的交互.
每个信号中至少携带了三个主要信息:UUID, Major, Minor,这三个信号组成了一个 iBeacon 的唯一标识符.

如何接收iBeacon?

作为iOS开发者,这里有一个先天优势,每一台iPhone设备都可以作为“发射者,所以我们需要准备两台iPhone手机,其中一台手机去AppStore下载AirBeacon应用,用于发射iBeacon广播信号(发射者),另外一台用于接收调试

实际场景肯定不是用iPhone设备作为发射,一般都有很多第三方的硬件厂商做这个,比较主流的生产商包括 Estimote 、 Aruba 和 Radius Networks .如果你正在打算使用 iBeacons ,上边的任何一家都会是个不错的选择。

一个基站主要有三部分标识:
  • 1. UUID,形如:206A2476-D4DB-42F0-BF73-030236F2C756。用来标识某一个公司。比如,某个房地产公司的全部的基站都用同一个UUID。
  • 2. major,用来标识某一类的beacon。比如这个房地产公司的北京的房子都设定为1,上海的都设定为2。
  • 3. minor,用来标识某一个特定的beacon。比如某栋楼的某个基站。

用这个房地产商开发的app就可以获取到UUID,和后面的major值和minor值。当app进入beacon的区域,探测到UUID:”206A2476-D4DB-42F0-BF73-030236F2C756“,major为1,minor为20。那么就表明这个用户在这个房地产商的北京楼盘的编号20的楼盘。这栋楼的说明文字、图片或者视频等就可以展现在用户面前。

iOS中相关API和使用方法,大致代码
  • 需要打开GPS定位和蓝牙,在Info.plist 增加 Privacy – Location Always Usage Description
  • iBeacon 的 API 是在 CoreLocation, 但iBeacon 必须要打开蓝牙,如果需要判断蓝牙,需要用到 CoreBluetooth 框架.
  • Monitoring和 Ranging 是两种监测方式,可以一起用,但是需要区分业务需求,两种一起用会有小坑.
  1. CLLocationManager
locationManager = CLLocationManager.init() 
// 遵循代理
locationManager?.delegate = self 
// 请求用户授权定位
locationManager?.requestAlwaysAuthorization()

2.CLBeaconRegion 的创建

//唯一标示,其实有三个,包括major,minor
let ZWUUID:UUID = UUID(uuidString: "7E66DA30-0A96-4DB5-A15B-066CE9032E70")!

beaconRegin = CLBeaconRegion(proximityUUID: ZWUUID, major: 6, minor: 3, identifier: "ZWIBeacon")
//通知退出和进入
beaconRegin?.notifyOnExit = true
beaconRegin?.notifyOnEntry = true

//补充说明:
//仅使用proximityUUID来初始化区域,major,minor值将作为通配符。只要是区域内的iBeacon的proximityUUID与此proximityUUID相同,不管major, minor是什么值,都能被检测到。
        CLBeaconRegion(proximityUUID:identifier:)
//使用proximityUUID和major来初始化区域,minor值将作为通配符。区域内的iBeacon的proximityUUID和major与此proximityUUID和major相同时,不论minor为何值,都能被检测到。
        CLBeaconRegion(proximityUUID:major:identifier:)
//使用proximityUUID, major, minor来初始化,只能检测到区域内相同proximityUUID, major, minor的iBeacon设备。
        CLBeaconRegion(proximityUUID:major: minor:identifier:)

3.可用两种方式检测区域 Monitoring或Ranging方式。

Monitoring方式: 可以用来在设备进入/退出某个地理区域时获得通知, 使用这种方法可以在应用程序的后台运行时检测iBeacon,但是只能同时检测20个region区域,并且不能够推测设备与iBeacon的距离。

locationManager?.startMonitoring(for: beaconRegin!)
    locationManager?.stopMonitoring(for: beaconRegin!)
    // 设备进入该区域时的回调
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {

    }
    // 设备退出该区域时的回调
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {

    }
    // 有错误产生时的回调
    func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {

    }

Ranging方式: 可以用来检测某区域内的所有iBeacons。

locationManager?.startRangingBeacons(in: beaconRegin!)
    locationManager?.stopRangingBeacons(in: beaconRegin!)
     // 检测到区域内的iBeacons时回调此函数,差不多1s刷新一次,这个方法会返回一个 CLBeacon 的数组,根据 CLBeacon 的 proximity 属性就可以判断设备和 beacon 之间的距离,
     // proximity 属性有四个可能的值,unknown、immediate、near 和 far, 另外 CLBeacon 还有 accuracy 和 rssi 两个属性能提供更详细的距离数据
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {

    }
    // 有错误时候的回调
    func locationManager(_ manager: CLLocationManager, rangingBeaconsDidFailFor region: CLBeaconRegion, withError error: Error) {

    }

注意:如果搜索不到设备广播,看看是不是因为UUID等唯一标示是不是不一样

更多想法讨论

1.距离是否准确

网上有许多围绕每次 ranging event(指的就是范围模式的 didRangeBeacons 回调) 所返回的 CLLocationAccuracy 值的讨论。人们常把这个值当作手机与发射器的实际距离值。以我的经验来看,你当然可以把这个值当作实际的距离,但它并不总是那么准确。苹果文档建议你首先利用 CLProxity 枚举值来初步判定设备距离的远近,然后再用 CLLocationAccuracy 的值来区分其中接近度相近的值。

2.监听超过 20 个设备

正像我前边介绍的那样,你现在只能监听 20 个 iBeacons ,如果你需要监听超过 20 个设备,你将需要在应用运行的过程中更改监听的设置,一种实现方案是用图表来展示你的 iBeacons 网络,在网络中定义最顶层的 iBeacons 以及如果这些彼此接近的情况下能够连接到的边界。这样你就可以快速的查找到最接近的 20 个 iBeacons 并监听他们。这需要很多的工作,但是定义一个这样的拓扑是一种实现 20 个 iBeacons 限制的方式。

一个完整的Beacon例子:
import UIKit
import CoreLocation
import CoreBluetooth

//蓝牙开启通知
// did centralManager enable notification
let BluetoothNotificationManagerEnable = "BabyNotificationAtCentralManagerEnable"

// 蓝牙弹框是需要弹出
let BluetoothAlertIsShow = "BluetoothAlertIsShow"

// 搜索到设备数组
typealias IbeaconSearchResults = (([CLBeacon]) -> ())

class IbeaconManager: NSObject {

    static let `default` = IbeaconManager()
    
    var searchResultsCallback: IbeaconSearchResults?
    
    fileprivate var beaconSendRegion: CLBeaconRegion! // 发送
    fileprivate var beaconReceiveRegion: CLBeaconRegion! // 接受
    fileprivate var locationManager: CLLocationManager!
    fileprivate var beaconPeripheralData: NSDictionary!
    fileprivate var peripheraManager: CBPeripheralManager!
    var location: Float = 0.0 //距离
    let beaconIdentifier = "ibeaconTest"
    let defaultUUIDString = "XXXX-XXXXXX-XXXXXX-XXXXXXXXXX"
    
    override init() {
        super.init()
        
        // 发射信号
        // 此处代码用另一部手机运行 模拟ibeacon设备发送信号
        beaconSendRegion = CLBeaconRegion(proximityUUID: UUID(uuidString: defaultUUIDString)!, major: 1234, minor: 5678, identifier: beaconIdentifier)
        beaconPeripheralData = beaconSendRegion.peripheralData(withMeasuredPower: nil)
        peripheraManager = CBPeripheralManager(delegate: self, queue: nil)
        

        // 接受信号
        locationManager = CLLocationManager()
        locationManager.delegate = self
        locationManager.requestAlwaysAuthorization()
        beaconReceiveRegion = CLBeaconRegion(proximityUUID: UUID(uuidString: defaultUUIDString)!, identifier: beaconIdentifier)
        beaconReceiveRegion.notifyEntryStateOnDisplay = true
        
        //请求一直允许定位
        locationManager.requestAlwaysAuthorization()
        beaconReceiveRegion.notifyEntryStateOnDisplay = true
    }
    
    /// 开始扫描
    func startRunningBeacons() {
        //开始扫描
        locationManager.startMonitoring(for: beaconReceiveRegion)
        locationManager.startRangingBeacons(in: beaconReceiveRegion)
    }
    
}

extension IbeaconManager: CLLocationManagerDelegate {
    
    //进入beacon区域
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        locationManager.startRangingBeacons(in: beaconReceiveRegion)
        print( "进入beacon区域")
    }
    
    //离开beacon区域
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        locationManager.stopRangingBeacons(in: beaconReceiveRegion)
        print("离开beacon区域")
    }
    
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
        //返回是扫描到的beacon设备数组,这里取第一个设备
        guard beacons.count > 0 else { return }
        
        self.searchResultsCallback?(beacons)
        
        // 下面为调试信息
        let beacon = beacons.first!
        print("major====",beacon.major)
        print("minor====",beacon.minor)
        //accuracy可以获取到当前距离beacon设备距离
        let location = String(format: "%.3f", beacon.accuracy)
        print( "距离第一个beacon\(location)m")
    }
    
    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)")
    }
}

extension IbeaconManager: CBPeripheralManagerDelegate {
    
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        
        switch peripheral.state {
        case .poweredOn:
            peripheraManager.startAdvertising(beaconPeripheralData as? [String : Any])
            print("蓝牙打开了!=============================")
            print(beaconReceiveRegion.proximityUUID)
            print(beaconReceiveRegion.major)
            print(beaconReceiveRegion.minor)
            print(beaconReceiveRegion.identifier)
            print("蓝牙打开了!=============================")
            UserDefaults.standard.set(true, forKey: BluetoothNotificationManagerEnable)
            UserDefaults.standard.synchronize()
        case .poweredOff:
            print("蓝牙未打开")
            UserDefaults.standard.set(false, forKey: BluetoothNotificationManagerEnable)
            UserDefaults.standard.synchronize()
        default: peripheraManager.stopAdvertising()
            
        }
    }
}

需要使用的地方

/// 开始搜索蓝牙列表 | 只要搜索到了就记录值 用于对比教师课程里的UUID是否跟这个一致 | 如果一致说明在蓝牙搜索的范围内 可以执行签到 如果没有则不在搜索范围内
    func startBlueToothSearch(blueToothNotOpen: (()->())?) {
        
        IbeaconManager.default.startRunningBeacons()
        IbeaconManager.default.searchResultsCallback = { (ibeacons) in
            var location = Double(1000)
            for ibeacon in ibeacons {
                let majorMinor = "\(ibeacon.major)\(ibeacon.minor)"
                // 值越小代表距离最近 ibeacon.accuracy 为距离
                if ibeacon.accuracy <= location {
                    location = ibeacon.accuracy
                }
                print("major====",ibeacon.major)
                print("minor====",ibeacon.minor)
                let location = String(format: "%.3f", ibeacon.accuracy)
                print( "距离beacon====\(location)m")
            }
        }
        
        // 检测蓝牙是否打开
        if let enable = UserDefaults.standard.value(forKey: BluetoothNotificationManagerEnable) as? Bool {
            if enable == false {
                blueToothNotOpen?()
            }
        }else {
            blueToothNotOpen?()
        }
    }