iBeacon调研记录(一) 主要介绍了iBeacon(Bluetooth Low Energy)是什么,使用场景,API介绍及注意事项。如果看过了上篇文章,相信你对iBeacon已经有了初步了解,这篇文章主要是利用上一篇所记录的内容写一个方便监视iBeacon设备信号动态变化和修改监视区域参数的工具。
简单描述一下需求:
实时监测到iBeacon信号变化
可以方便添加/删除/修改监视的iBeacon
从边界进入到监视区域会通知用户 (在前台:弹出提示框 在后台:本地推送消息)
刚开始就在监视区域通知用户 (在前台:弹出提示框 在后台:本地推送消息)
同一区域不重复性提示用户
看了上面的需求,首先我们要有一个列表用来展示所有iBeacon信号变化,当进行添加/修改操作的时候,需要跳转到一个新的页面去编辑,编辑完成之后再跳回到iBeacon列表页面,页面的简单操作描述就是这样。
最终在真机下的效果:
在开撸之前,还要了解一下CLBeaconRegion
和CLBeacon
:
CLBeaconRegion: 用来设置监视区域的行为特征(比如:设置notifyEntryStateOnDisplay=YES),还有存放监视区域的UUID,Major,Minor等信息。
CLBeacon: 相当于我们的iBeacon设备信息,我们不仅可以从中获取到UUID,Major,Minor,还能获取到accuracy(距离),RSSI(信号强度)等信息。
开撸!
首先我们要在新建工程的info.plist文件中添加Privacy - Location When In Use Usage Description
或 Privacy - Location Always Usage Description
来告诉系统我们要使用定位服务,苹果把iBeacon集成到了<CoreLocation/CoreLocation.h>
框架中,由CLLocationManager
来管理。
在iBeacon列表控制器,声明这三个属性:
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 26 27 28 29 30 31 32 @interface IBMBeaconViewController ()<CLLocationManagerDelegate, UITableViewDelegate, UITableViewDataSource> // 定位管理 @property (nonatomic, strong) CLLocationManager *locationManager; // 存放IBMBeaconItem的数组 @property (nonatomic, strong) NSMutableArray *beaconItems; // tableView视图 @property (weak, nonatomic) IBOutlet UITableView *beaconTableView; @end // 在实现中利用懒加载初始化,beaconTableView是在storyboard的控件,无需代码初始化 @implementation IBMBeaconViewController - (CLLocationManager *)locationManager { if (!_locationManager) { _locationManager = [[CLLocationManager alloc]init]; _locationManager.delegate = self; // iOS8后要手动授权 [_locationManager requestWhenInUseAuthorization]; } return _locationManager; } - (NSMutableArray *)beaconItems { if (!_beaconItems) { _beaconItems = [NSMutableArray array]; } return _beaconItems; } @end
beaconItems中存放的是IBMBeaconItem类的实例,IBMBeaconItem用来存放iBeacon的信息:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 #import <CoreLocation/CoreLocation.h> @interface IBMBeaconItem : NSObject<NSCoding> // 设备的名字(identifier) @property (strong, nonatomic, readonly) NSString *name; @property (strong, nonatomic, readonly) NSUUID *uuid; @property (assign, nonatomic, readonly) CLBeaconMajorValue majorValue; @property (assign, nonatomic, readonly) CLBeaconMinorValue minorValue; // 记录每次通知的时间 @property (nonatomic, strong) NSDate *lastNotifyDate; // 匹配的CLBeacon @property (nonatomic, strong) CLBeacon *beaconMatching; - (instancetype)initWithName:(NSString *)name uuid:(NSUUID *)uuid major:(CLBeaconMajorValue)major minor:(CLBeaconMinorValue)minor; /** 判断IBMBeaconItem实例的信息是否与检测到的区域相同 @param beaconRegion CLBeaconRegion区域实例 @return 判断结果 */ - (BOOL)isEqualToCLBeaconRegion:(CLBeaconRegion *)beaconRegion; /** 判断是否需要发送通知(当前时间 - 上次通知记录的时间(lastNotifyDate) > 规定的时间 就返回YES发送通知 否则NO) @return 判断结果 */ - (BOOL)shouldSendNotificaiton; /** 判断IBMBeaconItem实例的信息是否与检测到的设备信息相同 @param beacon 设备实例 @return 判断结果 */ - (BOOL)isEqualToCLBeacon:(CLBeacon *)beacon; @end // 下面是实现类.m #import "IBMBeaconItem.h" static NSString *const ibeaconName = @"ibeaconName"; static NSString *const iBeaconUUID = @"iBeaconUUID"; static NSString *const iBeaconMajor = @"iBeaconMajor"; static NSString *const iBeaconMinor = @"iBeaconMinor"; static NSString *const iBeaconLastNotifyDate = @"iBeaconLastNotifyDate"; // 过期时间间隔,默认是10.0s (当前时间 - 上次发送通知时间 > expiredTime 就发送通知) static const NSTimeInterval expiredTime = 10.0; @implementation IBMBeaconItem - (instancetype)initWithName:(NSString *)name uuid:(NSUUID *)uuid major:(CLBeaconMajorValue)major minor:(CLBeaconMinorValue)minor { self = [super init]; if (self) { _name = name; _uuid = uuid; _majorValue = major; _minorValue = minor; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _name = [aDecoder decodeObjectForKey:ibeaconName]; _uuid = [aDecoder decodeObjectForKey:iBeaconUUID]; _majorValue = [[aDecoder decodeObjectForKey:iBeaconMajor] unsignedIntegerValue]; _minorValue = [[aDecoder decodeObjectForKey:iBeaconMinor] unsignedIntegerValue]; _lastNotifyDate = [aDecoder decodeObjectForKey:iBeaconLastNotifyDate]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.name forKey:ibeaconName]; [aCoder encodeObject:self.uuid forKey:iBeaconUUID]; [aCoder encodeObject:@(self.majorValue) forKey:iBeaconMajor]; [aCoder encodeObject:@(self.minorValue) forKey:iBeaconMinor]; [aCoder encodeObject:self.lastNotifyDate forKey:iBeaconLastNotifyDate]; } - (BOOL)isEqualToCLBeaconRegion:(CLBeaconRegion *)beaconRegion { return [self isEqualToBeaconInfo:beaconRegion]; } - (BOOL)isEqualToBeaconInfo:(id)beaconInfo { if ([[[beaconInfo valueForKey:@"proximityUUID"] UUIDString] isEqualToString:[self.uuid UUIDString]] && [[beaconInfo valueForKey:@"major"] isEqual: @(self.majorValue)] && [[beaconInfo valueForKey:@"minor"] isEqual: @(self.minorValue)]) { return YES; } else { return NO; } } - (BOOL)isEqualToCLBeacon:(CLBeacon *)beacon { return [self isEqualToBeaconInfo:beacon]; } - (BOOL)shouldSendNotificaiton { if (self.lastNotifyDate == nil) { [self updateLastNotifyDate]; return YES; }else { NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:self.lastNotifyDate]; BOOL isNeedUpdate = timeInterval > expiredTime ? YES : NO; if (isNeedUpdate) { [self updateLastNotifyDate]; return YES; }else { return NO; } } } - (void)setLastNotifyDate:(NSDate *)lastNotifyDate { if (lastNotifyDate) { _lastNotifyDate = lastNotifyDate; } } - (void)updateLastNotifyDate { self.lastNotifyDate = [[NSDate alloc] initWithTimeIntervalSinceNow:0]; } - (NSString *)description { NSString *descriptionStr = [NSString stringWithFormat:@"name=%@\n uuid=%@ \n majorValue=%d minorValue=%d \n lastNotifyDate=%@", _name,_uuid,_majorValue,_minorValue, _lastNotifyDate]; return descriptionStr; } @end
我们不希望每次添加完iBeacon监视区域重新启动程序后添加的数据就没了,所以要做本地持久化处理,而且要在工程的任何地方都要能够访问到(如:Appdelegate中),所以创建了一个用单例初始化的类IBMBeaconItemManager来管理:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #import "IBMBeaconItem.h" typedef void(^TraverseBeaconItemHandle)(IBMBeaconItem *item); @interface IBMBeaconItemManager : NSObject // 存放IBMBeaconItem对象转化成Data类型后的数组 @property (nonatomic, copy, readonly) NSArray<NSData *> *storeItemsData; // 存放IBMBeaconItem对象的数组 @property (nonatomic, copy, readonly) NSArray<IBMBeaconItem *> *storeItems; + (instancetype)sharedInstanced; /** 同步本地数据,比如在客户端我们在做添加/删除/修改 操作的时候,要与本地的数据同步一下 @param latestItems 要同步的数组<IBMBeaconItem *> */ - (void)syncStoreItemsWithLatestItems:(NSArray<IBMBeaconItem *> *)latestItems; /** 遍历本地存放的ItemData,把Data转化成IBMBeaconItem并每一次遍历都会把IBMBeaconItem通过TraverseBeaconItemHandle传到调用的地方,详情请看实现代码 @param itemHandle Block */ - (void)TraverseStoreItemsHandle:(TraverseBeaconItemHandle)itemHandle; @end // IBMBeaconItemManager.m实现代码 #import "IBMBeaconItemManager.h" // 本地存储Key值 static NSString *const beaconItemsStoreKey = @"beaconItemsStoreKey"; @implementation IBMBeaconItemManager @synthesize storeItemsData = _storeItemsData; @synthesize storeItems = _storeItems; + (instancetype)sharedInstanced { static IBMBeaconItemManager *manager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager = [[self alloc]init]; }); return manager; } - (void)TraverseStoreItemsHandle:(TraverseBeaconItemHandle)itemHandle { if (self.storeItemsData) { for (NSData *itemData in _storeItemsData) { IBMBeaconItem *ibeaconItem = [NSKeyedUnarchiver unarchiveObjectWithData:itemData]; itemHandle(ibeaconItem); } } } - (void)syncStoreItemsWithLatestItems:(NSArray<IBMBeaconItem *> *)latestItems { NSMutableArray *cacheItemsData = [NSMutableArray array]; for (IBMBeaconItem *item in latestItems) { NSData *itemData = [NSKeyedArchiver archivedDataWithRootObject:item]; [cacheItemsData addObject:itemData]; } _storeItemsData = [cacheItemsData copy]; _storeItems = [latestItems copy]; [[NSUserDefaults standardUserDefaults] setObject:_storeItemsData forKey:beaconItemsStoreKey]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (NSArray<NSData *> *)storeItemsData { if (!_storeItemsData) { _storeItemsData = [[NSUserDefaults standardUserDefaults] objectForKey:beaconItemsStoreKey]; } return _storeItemsData; } - (NSArray<IBMBeaconItem *> *)storeItems { if (!_storeItems) { NSMutableArray *mutableStoreItems = [NSMutableArray array]; if (self.storeItemsData) { for (NSData *itemData in _storeItemsData) { IBMBeaconItem *ibeaconItem = [NSKeyedUnarchiver unarchiveObjectWithData:itemData]; [mutableStoreItems addObject:ibeaconItem]; } } _storeItems = [mutableStoreItems copy]; } return _storeItems; } @end
有了IBMBeaconItem和IBMBeaconItemManager后,进入iBeacon列表控制器,在viewDidload中调用loadIbeaconItem方法:
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 26 27 28 29 30 31 #pragma mark - 步骤1 - (void)loadIbeaconItem { // 读取本地数据 [[IBMBeaconItemManager sharedInstanced] TraverseStoreItemsHandle:^(IBMBeaconItem *item) { // 每取到一个IBMBeaconItem 开始对它进行监听 [self startMonitoringWithItem:item]; // 添加到tableView的资源数组 [self.beaconItems addObject:item]; }]; } #pragma mark - 步骤2 - (void)startMonitoringWithItem:(IBMBeaconItem *)ibeaconItem { CLBeaconRegion *beaconRegion = [self generateBeaconRegionWithItem:ibeaconItem]; [self.locationManager startMonitoringForRegion:beaconRegion]; [self.locationManager startRangingBeaconsInRegion:beaconRegion]; } #pragma mark - 步骤3 - (CLBeaconRegion *)generateBeaconRegionWithItem:(IBMBeaconItem *)ibeaconItem { // 生成iBeacon区域实例,表示我们要监视的区域 CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc]initWithProximityUUID:ibeaconItem.uuid major:ibeaconItem.majorValue minor:ibeaconItem.minorValue identifier:ibeaconItem.name]; // 从黑屏->亮屏 通知- (void)locationManager:didDetermineState:forRegion:代理方法 beaconRegion.notifyEntryStateOnDisplay = YES; return beaconRegion; }
这样,我们就开始对iBeacon区域开启了监视,剩下的就是我们要在Appdelegate类中去实现locationManager:didEnterRegion:
,locationManager:didExitRegion:
,locationManager:didDetermineState:forRegion:
代理方法,为什么要写在Appdelegate类中,请看上篇文章 ,这里要注意一下:当我将window的rootViewController设置为iBeacon列表控制器,并把所有代理方法实现都写到iBeacon列表控制器中,关闭程序后,依然会调用到代理方法,如果iBeacon列表控制器不设置为window的rootViewController就无法调用到了。所以我们把剩下监测到iBeacon通知的逻辑写到Appdelegate类中:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 @interface AppDelegate ()<CLLocationManagerDelegate, UIApplicationDelegate> @property (nonatomic, strong) CLLocationManager *locationManager; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.locationManager.delegate = self; return YES; } #pragma mark - 本地通知接收 - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [self alertMessage:notification.alertBody]; } #pragma mark - CLLocationManagerDelegate // 进入到iBeacon区域(会有延迟,大概30s) - (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region { if ([region isKindOfClass:[CLBeaconRegion class]]) { CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region; [self matchIbeaconRegion:beaconRegion]; } } - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { NSLog(@"离开iBeacon区域"); } // requestStateForRegion:方法调用就会触发该代理方法,另外从黑屏到亮屏过程也会触发 - (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region { switch (state) { case CLRegionStateUnknown: { NSLog(@"未知"); } break; case CLRegionStateInside: { NSLog(@"在iBeacon区域里面"); if ([region isKindOfClass:[CLBeaconRegion class]]) { CLBeaconRegion *beaconRegion = (CLBeaconRegion *)region; [self matchIbeaconRegion:beaconRegion]; } } break; case CLRegionStateOutside: { NSLog(@"在iBeacon区域外面"); } break; } } // CLBeaconRegion 来匹配与之对应的 IBMBeaconItem - (void)matchIbeaconRegion:(CLBeaconRegion *)ibeaconRegion { NSArray<IBMBeaconItem *> * beaconItems = [IBMBeaconItemManager sharedInstanced].storeItems; for (IBMBeaconItem *item in beaconItems) { if ([item isEqualToCLBeaconRegion:ibeaconRegion]) { [self sendLocalNotificationWithIbeaconItem:item]; } } } // 弹出提示框 - (void)alertMessage:(NSString *)message { UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:message delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil]; [alertView show]; } // 发送本地通知(弹出框) - (void)sendLocalNotificationWithIbeaconItem:(IBMBeaconItem *)ibeaconItem { if ([ibeaconItem shouldSendNotificaiton]) { NSString *notifyString = [NSString stringWithFormat:@"进入%@的iBeacon区域",ibeaconItem.name]; UIApplicationState appState = [UIApplication sharedApplication].applicationState; switch (appState) { case UIApplicationStateActive: // 在前台 { [self alertMessage:notifyString]; break; } case UIApplicationStateBackground: // 在后台 { // 通知的代码 UILocalNotification *notice = [[UILocalNotification alloc] init]; notice.alertBody = notifyString; // notice.alertAction = @"iBeacon区域提醒"; [[UIApplication sharedApplication] scheduleLocalNotification:notice]; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; break; } default: break; } } } - (CLLocationManager *)locationManager { if (!_locationManager) { _locationManager = [[CLLocationManager alloc]init]; } return _locationManager; } @end
核心的监测代码就完成啦,剩下的展示内容,新增/修改/删除操作具体实现过程可参考Demo !