blob: 73304dfeceaf2033894655f7602be0e931e841c7 [file] [log] [blame]
//
// NTViewController.m
// HiBeacons
//
// Created by Nick Toumpelis on 2013-10-06.
// Copyright (c) 2013 Nick Toumpelis.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#import "NTViewController.h"
static NSString * const kUUID = @"00000000-0000-0000-0000-000000000000";
static NSString * const kIdentifier = @"SomeIdentifier";
static NSString * const kOperationCellIdentifier = @"OperationCell";
static NSString * const kBeaconCellIdentifier = @"BeaconCell";
static NSString * const kAdvertisingOperationTitle = @"Advertising";
static NSString * const kRangingOperationTitle = @"Ranging";
static NSUInteger const kNumberOfSections = 2;
static NSUInteger const kNumberOfAvailableOperations = 2;
static CGFloat const kOperationCellHeight = 44;
static CGFloat const kBeaconCellHeight = 52;
static NSString * const kBeaconSectionTitle = @"Looking for beacons...";
static CGPoint const kActivityIndicatorPosition = (CGPoint){205, 12};
static NSString * const kBeaconsHeaderViewIdentifier = @"BeaconsHeader";
typedef NS_ENUM(NSUInteger, NTSectionType) {
NTOperationsSection,
NTDetectedBeaconsSection
};
typedef NS_ENUM(NSUInteger, NTOperationsRow) {
NTAdvertisingRow,
NTRangingRow
};
@interface NTViewController ()
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) CLBeaconRegion *beaconRegion;
@property (nonatomic, strong) CBPeripheralManager *peripheralManager;
@property (nonatomic, strong) NSArray *detectedBeacons;
@property (nonatomic, weak) UISwitch *advertisingSwitch;
@property (nonatomic, weak) UISwitch *rangingSwitch;
@end
@implementation NTViewController
#pragma mark - Beacon ranging
- (void)createBeaconRegion
{
if (self.beaconRegion)
return;
NSUUID *proximityUUID = [[NSUUID alloc] initWithUUIDString:kUUID];
self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:proximityUUID identifier:kIdentifier];
}
- (void)turnOnRanging
{
NSLog(@"Turning on ranging...");
if (![CLLocationManager isRangingAvailable]) {
NSLog(@"Couldn't turn on ranging: Ranging is not available.");
self.rangingSwitch.on = NO;
return;
}
if (self.locationManager.rangedRegions.count > 0) {
NSLog(@"Didn't turn on ranging: Ranging already on.");
return;
}
[self createBeaconRegion];
[self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
NSLog(@"Ranging turned on for region: %@.", self.beaconRegion);
}
- (void)changeRangingState:sender
{
UISwitch *theSwitch = (UISwitch *)sender;
if (theSwitch.on) {
[self startRangingForBeacons];
} else {
[self stopRangingForBeacons];
}
}
- (void)startRangingForBeacons
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.detectedBeacons = [NSArray array];
[self turnOnRanging];
}
- (void)stopRangingForBeacons
{
if (self.locationManager.rangedRegions.count == 0) {
NSLog(@"Didn't turn off ranging: Ranging already off.");
return;
}
[self.locationManager stopRangingBeaconsInRegion:self.beaconRegion];
NSIndexSet *deletedSections = [self deletedSections];
self.detectedBeacons = [NSArray array];
[self.beaconTableView beginUpdates];
if (deletedSections)
[self.beaconTableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationFade];
[self.beaconTableView endUpdates];
NSLog(@"Turned off ranging.");
}
#pragma mark - Index path management
- (NSArray *)indexPathsOfRemovedBeacons:(NSArray *)beacons
{
NSMutableArray *indexPaths = nil;
NSUInteger row = 0;
for (CLBeacon *existingBeacon in self.detectedBeacons) {
BOOL stillExists = NO;
for (CLBeacon *beacon in beacons) {
if ((existingBeacon.major.integerValue == beacon.major.integerValue) &&
(existingBeacon.minor.integerValue == beacon.minor.integerValue)) {
stillExists = YES;
break;
}
}
if (!stillExists) {
if (!indexPaths)
indexPaths = [NSMutableArray new];
[indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
}
row++;
}
return indexPaths;
}
- (NSArray *)indexPathsOfInsertedBeacons:(NSArray *)beacons
{
NSMutableArray *indexPaths = nil;
NSUInteger row = 0;
for (CLBeacon *beacon in beacons) {
BOOL isNewBeacon = YES;
for (CLBeacon *existingBeacon in self.detectedBeacons) {
if ((existingBeacon.major.integerValue == beacon.major.integerValue) &&
(existingBeacon.minor.integerValue == beacon.minor.integerValue)) {
isNewBeacon = NO;
break;
}
}
if (isNewBeacon) {
if (!indexPaths)
indexPaths = [NSMutableArray new];
[indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
}
row++;
}
return indexPaths;
}
- (NSArray *)indexPathsForBeacons:(NSArray *)beacons
{
NSMutableArray *indexPaths = [NSMutableArray new];
for (NSUInteger row = 0; row < beacons.count; row++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
}
return indexPaths;
}
- (NSIndexSet *)insertedSections
{
if (self.rangingSwitch.on && [self.beaconTableView numberOfSections] == kNumberOfSections - 1) {
return [NSIndexSet indexSetWithIndex:1];
} else {
return nil;
}
}
- (NSIndexSet *)deletedSections
{
if (!self.rangingSwitch.on && [self.beaconTableView numberOfSections] == kNumberOfSections) {
return [NSIndexSet indexSetWithIndex:1];
} else {
return nil;
}
}
- (NSArray *)filteredBeacons:(NSArray *)beacons
{
// Filters duplicate beacons out; this may happen temporarily if the originating device changes its Bluetooth id
NSMutableArray *mutableBeacons = [beacons mutableCopy];
NSMutableSet *lookup = [[NSMutableSet alloc] init];
for (int index = 0; index < [beacons count]; index++) {
CLBeacon *curr = [beacons objectAtIndex:index];
NSString *identifier = [NSString stringWithFormat:@"%@/%@", curr.major, curr.minor];
// this is very fast constant time lookup in a hash table
if ([lookup containsObject:identifier]) {
[mutableBeacons removeObjectAtIndex:index];
} else {
[lookup addObject:identifier];
}
}
return [mutableBeacons copy];
}
#pragma mark - Beacon ranging delegate methods
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
if (![CLLocationManager locationServicesEnabled]) {
NSLog(@"Couldn't turn on ranging: Location services are not enabled.");
self.rangingSwitch.on = NO;
return;
}
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized) {
NSLog(@"Couldn't turn on ranging: Location services not authorised.");
self.rangingSwitch.on = NO;
return;
}
self.rangingSwitch.on = YES;
}
- (void)locationManager:(CLLocationManager *)manager
didRangeBeacons:(NSArray *)beacons
inRegion:(CLBeaconRegion *)region {
NSArray *filteredBeacons = [self filteredBeacons:beacons];
if (filteredBeacons.count == 0) {
NSLog(@"No beacons found nearby.");
} else {
NSLog(@"Found %lu %@.", (unsigned long)[filteredBeacons count],
[filteredBeacons count] > 1 ? @"beacons" : @"beacon");
}
NSIndexSet *insertedSections = [self insertedSections];
NSIndexSet *deletedSections = [self deletedSections];
NSArray *deletedRows = [self indexPathsOfRemovedBeacons:filteredBeacons];
NSArray *insertedRows = [self indexPathsOfInsertedBeacons:filteredBeacons];
NSArray *reloadedRows = nil;
if (!deletedRows && !insertedRows)
reloadedRows = [self indexPathsForBeacons:filteredBeacons];
self.detectedBeacons = filteredBeacons;
[self.beaconTableView beginUpdates];
if (insertedSections)
[self.beaconTableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationFade];
if (deletedSections)
[self.beaconTableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationFade];
if (insertedRows)
[self.beaconTableView insertRowsAtIndexPaths:insertedRows withRowAnimation:UITableViewRowAnimationFade];
if (deletedRows)
[self.beaconTableView deleteRowsAtIndexPaths:deletedRows withRowAnimation:UITableViewRowAnimationFade];
if (reloadedRows)
[self.beaconTableView reloadRowsAtIndexPaths:reloadedRows withRowAnimation:UITableViewRowAnimationNone];
[self.beaconTableView endUpdates];
}
#pragma mark - Beacon advertising
- (void)turnOnAdvertising
{
if (self.peripheralManager.state != CBPeripheralManagerStatePoweredOn) {
NSLog(@"Peripheral manager is off.");
self.advertisingSwitch.on = NO;
return;
}
time_t t;
srand((unsigned) time(&t));
CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:self.beaconRegion.proximityUUID
major:rand()
minor:rand()
identifier:self.beaconRegion.identifier];
NSDictionary *beaconPeripheralData = [region peripheralDataWithMeasuredPower:nil];
[self.peripheralManager startAdvertising:beaconPeripheralData];
NSLog(@"Turning on advertising for region: %@.", region);
}
- (void)changeAdvertisingState:sender
{
UISwitch *theSwitch = (UISwitch *)sender;
if (theSwitch.on) {
[self startAdvertisingBeacon];
} else {
[self stopAdvertisingBeacon];
}
}
- (void)startAdvertisingBeacon
{
NSLog(@"Turning on advertising...");
[self createBeaconRegion];
if (!self.peripheralManager)
self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
[self turnOnAdvertising];
}
- (void)stopAdvertisingBeacon
{
[self.peripheralManager stopAdvertising];
NSLog(@"Turned off advertising.");
}
#pragma mark - Beacon advertising delegate methods
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheralManager error:(NSError *)error
{
if (error) {
NSLog(@"Couldn't turn on advertising: %@", error);
self.advertisingSwitch.on = NO;
return;
}
if (peripheralManager.isAdvertising) {
NSLog(@"Turned on advertising.");
self.advertisingSwitch.on = YES;
}
}
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheralManager
{
if (peripheralManager.state != CBPeripheralManagerStatePoweredOn) {
NSLog(@"Peripheral manager is off.");
self.advertisingSwitch.on = NO;
return;
}
NSLog(@"Peripheral manager is on.");
[self turnOnAdvertising];
}
#pragma mark - Table view functionality
- (NSString *)detailsStringForBeacon:(CLBeacon *)beacon
{
NSString *proximity;
switch (beacon.proximity) {
case CLProximityNear:
proximity = @"Near";
break;
case CLProximityImmediate:
proximity = @"Immediate";
break;
case CLProximityFar:
proximity = @"Far";
break;
case CLProximityUnknown:
default:
proximity = @"Unknown";
break;
}
NSString *format = @"%@, %@ • %@ • %f • %li";
return [NSString stringWithFormat:format, beacon.major, beacon.minor, proximity, beacon.accuracy, beacon.rssi];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = nil;
switch (indexPath.section) {
case NTOperationsSection: {
cell = [tableView dequeueReusableCellWithIdentifier:kOperationCellIdentifier];
switch (indexPath.row) {
case NTAdvertisingRow:
cell.textLabel.text = kAdvertisingOperationTitle;
self.advertisingSwitch = (UISwitch *)cell.accessoryView;
[self.advertisingSwitch addTarget:self
action:@selector(changeAdvertisingState:)
forControlEvents:UIControlEventValueChanged];
break;
case NTRangingRow:
default:
cell.textLabel.text = kRangingOperationTitle;
self.rangingSwitch = (UISwitch *)cell.accessoryView;
[self.rangingSwitch addTarget:self
action:@selector(changeRangingState:)
forControlEvents:UIControlEventValueChanged];
break;
}
}
break;
case NTDetectedBeaconsSection:
default: {
CLBeacon *beacon = self.detectedBeacons[indexPath.row];
cell = [tableView dequeueReusableCellWithIdentifier:kBeaconCellIdentifier];
if (!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:kBeaconCellIdentifier];
cell.textLabel.text = beacon.proximityUUID.UUIDString;
cell.detailTextLabel.text = [self detailsStringForBeacon:beacon];
cell.detailTextLabel.textColor = [UIColor grayColor];
}
break;
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
if (self.rangingSwitch.on) {
return kNumberOfSections; // All sections visible
} else {
return kNumberOfSections - 1; // Beacons section not visible
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
switch (section) {
case NTOperationsSection:
return kNumberOfAvailableOperations;
case NTDetectedBeaconsSection:
default:
return self.detectedBeacons.count;
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
switch (section) {
case NTOperationsSection:
return nil;
case NTDetectedBeaconsSection:
default:
return kBeaconSectionTitle;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
switch (indexPath.section) {
case NTOperationsSection:
return kOperationCellHeight;
case NTDetectedBeaconsSection:
default:
return kBeaconCellHeight;
}
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UITableViewHeaderFooterView *headerView =
[[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:kBeaconsHeaderViewIdentifier];
// Adds an activity indicator view to the section header
UIActivityIndicatorView *indicatorView =
[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[headerView addSubview:indicatorView];
indicatorView.frame = (CGRect){kActivityIndicatorPosition, indicatorView.frame.size};
[indicatorView startAnimating];
return headerView;
}
@end