iOS制作iBeacon相关SDK,在程序杀死后无法收到本地通知

2,613 阅读6分钟

1.iBeacon简介

iBeacon 是苹果公司2013年9月发布的移动设备用OS(iOS7)上配备的新功能。其工作方式是,配备有 低功耗蓝牙(BLE)通信功能的设备使用BLE技术向周围发送自己特有的ID,接收到该ID的应用软件会根据该ID采取一些行动。比如,在店铺里设置iBeacon通信模块的话,便可让iPhone和iPad上运行一资讯告知服务器,或者由服务器向顾客发送折扣券及进店积分。此外,还可以在家电发生故障或停止工作时使用iBeacon向应用软件发送资讯。

2.iBeacon权限相关

在Signing & Capabilities 中添加 Background Modes 然后再勾选以下选项 1241650428892_.pic.jpg 在info.plist中添加相应的权限

image.png

3.iBeacon相关代码实现

在自己的类中增加以下代码

@property (retain, nonatomic) CLLocationManager *locationManager;

创建 CLLocationManager

self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.activityType = CLActivityTypeFitness;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = 100;
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0){
  [self.locationManager requestAlwaysAuthorization];
}
// 允许后台获取用户位置(iOS9.0)
if([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) {
 // 一定要勾选后台模式 location updates 否者程序奔溃  
 self.locationManager.allowsBackgroundLocationUpdates = YES;
}
self.locationManager.pausesLocationUpdatesAutomatically = NO;
[self.locationManager startUpdatingLocation];

创建CLBeaconRegion对象

CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:@"xxxx"] major:1 minor:2 identifier:@"SomeIdentifier"];
region.notifyEntryStateOnDisplay = YES;
region.notifyOnEntry = YES;
region.notifyOnExit = YES;
[self.locationManager startMonitoringForRegion:region];

iBeacon接收者提供了两种方式来接收iBeacon信号

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

// 开始检测区域
[self.locationManager startMonitoringForRegion:beaconRegion]; 

// 停止检测区域
[self.locationManager stopMonitoringForRegion:beaconRegion]; 

// Monitoring成功对应回调函数
- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region;

//设备状态改变回调
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(nonnull CLRegion *)region;

// 设备进入该区域时的回调
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region;

// 设备退出该区域时的回调
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region;

// Monitoring有错误产生时的回调
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(nullable CLRegion *)region withError:(NSError *)error;

Ranging: iOS 7之后提供的 API, 用于确定设备的近似距离iBeacon 技术,可以用来检测某区域内的所有iBeacons,并且可以精度估计发射者与接收者的距离。

// 开始检测区域
[self.locationManager startRangingBeaconsInRegion:beaconRegion];

// 停止检测区域
[self.locationManager stopRangingBeaconsInRegion:beaconRegion];

// Ranging成功对应回调函数  1秒钟执行1次
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray<CLBeacon *> *)beacons inRegion:(CLBeaconRegion *)region 

// Ranging有错误产生时的回调
- (void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion:(CLBeaconRegion *)region withError:(NSError *)error

我们要实现后台监听和app被kill掉也能监听就需要使用 Monitoring 方式 以下这3个方法是在后台或者app被kill掉也能回调

//设备状态改变回调
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(nonnull CLRegion *)region;

// 设备进入该区域时的回调
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region;

// 设备退出该区域时的回调
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region;

给app添加本地通知

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
// 标题
content.title = @"发现beacon设备";
content.subtitle = @"";
// 内容
content.body = @"";
// 默认声音
//content.sound = [UNNotificationSound defaultSound];
// 添加自定义声音
content.sound = [UNNotificationSound soundNamed:@"Alert_ActivityGoalAttained_Salient_Haptic.caf"];
// 角标 (我这里测试的角标无效,暂时没找到原因)
content.badge = @1;
// 多少秒后发送,可以将固定的日期转化为时间
NSTimeInterval time = [[NSDate dateWithTimeIntervalSinceNow:2] timeIntervalSinceNow];
//NSTimeInterval time = 10;
// repeats,是否重复,如果重复的话时间必须大于60s,要不会报错
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:time repeats:NO];
// 添加通知的标识符,可以用于移除,更新等操作
NSString *identifier = @"noticeId";
// 通知请求
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
//添加通知
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
    NSLog(@"error:%@",error);
}];

然后把本地通知写在Monitoring的3个方法里面,就可以做到后台和kill后都可以监听得到了。

4.iBeacon代码处于SDK如何实现后台和kill之后的监听

如果你是把iBeacon相关代码封装成SDK(Frameworks),你会发现以上代码实现之后是无法在程序kill之后收到本地通知的,为什么呢?明明写在Demo里面是可以的,下面我们来看下怎么解决?

iBeacon相关SDK:

一般我们制作SDK(Frameworks 静态库)都是暴露一些方法出来供别人使用,内部实现是无法看到的,像有 init , startAction, stopAction ,releseAction方法等等,一般init方法会在AppDelegate中初始化,其他方法都是在特定的时机触发和使用。

当前APP处于kill状态下(完全退出)。扫描的方法就必须在AppDelegatedidFinishLaunchingWithOptions中实现,否则就无法在kill的状态下收到本地通知内容。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 开始检测区域
    [self.locationManager startMonitoringForRegion:beaconRegion];
}

既然是这样那我们直接把扫描写在这里不就可以了,一般我们都是在这里进行初始化SDK,后续的开始扫描工作都需要根据各自的业务去进行启动的,但是又必须在这里面实现startMonitoringForRegion方式,所以针对SDK实现iBeacon的方式,我加了一个监听处理。

1.在SDK的Manager中增加代理

备注: IBeaconManager 是自己SDK暴露出去的管理器对象,每个人的不一样。

@protocol BeaconRegionDelegate <NSObject>

/** 进入区域
* @param manager 管理器
* @param region 区域
*/
- (void)beaconManager:(IBeaconManager *)manager
       didEnterRegion:(CLRegion *)region;
         
/** 退出区域
* @param manager 管理器
* @param region 区域
*/
- (void)beaconManager:(IBeaconManager *)manager
        didExitRegion:(CLRegion *)region;
        
/** 状态改变
* @param manager 管理器
* @param state 区域状态
* @param region 区域
*/        
- (void)beaconManager:(IBeaconManager *)manager
    didDetermineState:(CLRegionState)state
            forRegion:(CLRegion *)region;

@end

IBeaconManager.h类中增加以下方法,把需要监听的对象传进来

/// 增加代理属性
@property (nonatomic, weak) id<BeaconRegionDelegate> delegate;

/* 用于初始化接收后台区域监听回调类,此函数以及handler实体类都必须是与AppDelegate随同启动,否则无法接收到后台beacon推送
 * @param handler 用于接收后台区域监听回调函数的类。传入的handler类用作接收回调(该类务必随程序的启动即初始化,否则无法接收到回调,默认handler为AppDelegate)
 */
- (void)regionHandler:(id<BeaconRegionDelegate>)handler;

IBeaconManager.m中实现regionHandler方法

- (void)regionHandler:(id<BeaconRegionDelegate>)handler {
    self.delegate = handler;
    //开启扫描
    /*
    建议创建一个临时的beaconRegion来使用,使用后立马调用stopMonitoringForRegion方法关闭,
    毕竟只是为了让程序初始化时候能响应扫描区域而已,然后在3个响应方法中做identifier对比剔除,如果是自己临时创建的beaconRegion的identifier就可以return了,不响应本地通知。
    */
    [self.locationManager startMonitoringForRegion:beaconRegion];
    [self.locationManager stopMonitoringForRegion:beaconRegion];
    
}

2.在SDK里面的CLLocationManagerDelegate方法回传状态

- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(nonnull CLRegion *)region {
    if([region.identifier isEqualToString:beaconRegion.identifier]) {
        return;
    }
    if (self.delegate && [self.delegate respondsToSelector:@selector(beaconManager:didDetermineState:forRegion:)]) {
        [self.delegate beaconManager:self didDetermineState:state forRegion:region];
    }
}

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
    if([region.identifier isEqualToString:beaconRegion.identifier]) {
        return;
    }
    if (self.delegate && [self.delegate respondsToSelector:@selector(beaconManager:didEnterRegion:)]) {
        [self.delegate beaconManager:self didEnterRegion:region];
    }
}

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
    if([region.identifier isEqualToString:beaconRegion.identifier]) {
        return;
    }
    if (self.delegate && [self.delegate respondsToSelector:@selector(beaconManager:didExitRegion:)]) {
        [self.delegate beaconManager:self didExitRegion:region];
    }
}

regionHandler如果你传的是AppDelegate进来,那你在AppDelegate实现BeaconRegionDelegate方法即可。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[IBeaconManager manager] regionHandler:self];
}

按照以上方法就可以实现SDK使用IBeacon程序在后台或者被kill之后也能获取本地通知了。

至于为什么直接在demo中实iBeacon可以收到本地通知,而在SDK中实现就无法收到本地通知原因。

个人猜测如下:

SDK是一个的单独的模块,是引入进app中使用的属于是外部关联,所以需要在程序初始化的时候开启扫描,直接在app中实现iBeacon操作是直接作用于这个应用当中的,所以不需要在程序初始化的时候扫描后续其他地方触发也是可以收到系统监听。

由于是第一次研究iBeacon,以上属于个人的想法,如果有大神有更加专业的解释和更好的方法欢迎评论交流。