blob: 73304dfeceaf2033894655f7602be0e931e841c7 [file] [log] [blame]
Nick Toumpelis02e5ae62013-10-06 16:36:34 +02001//
2// NTViewController.m
3// HiBeacons
4//
5// Created by Nick Toumpelis on 2013-10-06.
6// Copyright (c) 2013 Nick Toumpelis.
7//
8// Permission is hereby granted, free of charge, to any person obtaining a copy
9// of this software and associated documentation files (the "Software"), to deal
10// in the Software without restriction, including without limitation the rights
11// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12// copies of the Software, and to permit persons to whom the Software is
13// furnished to do so, subject to the following conditions:
14//
15// The above copyright notice and this permission notice shall be included in
16// all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24// THE SOFTWARE.
25//
26
27#import "NTViewController.h"
28
29static NSString * const kUUID = @"00000000-0000-0000-0000-000000000000";
30static NSString * const kIdentifier = @"SomeIdentifier";
Nick Toumpelis0c019102013-10-20 22:28:05 +020031
32static NSString * const kOperationCellIdentifier = @"OperationCell";
33static NSString * const kBeaconCellIdentifier = @"BeaconCell";
34
35static NSString * const kAdvertisingOperationTitle = @"Advertising";
36static NSString * const kRangingOperationTitle = @"Ranging";
37static NSUInteger const kNumberOfSections = 2;
38static NSUInteger const kNumberOfAvailableOperations = 2;
39static CGFloat const kOperationCellHeight = 44;
40static CGFloat const kBeaconCellHeight = 52;
41static NSString * const kBeaconSectionTitle = @"Looking for beacons...";
42static CGPoint const kActivityIndicatorPosition = (CGPoint){205, 12};
43static NSString * const kBeaconsHeaderViewIdentifier = @"BeaconsHeader";
44
45typedef NS_ENUM(NSUInteger, NTSectionType) {
46 NTOperationsSection,
47 NTDetectedBeaconsSection
48};
49
50typedef NS_ENUM(NSUInteger, NTOperationsRow) {
51 NTAdvertisingRow,
52 NTRangingRow
53};
Nick Toumpelis02e5ae62013-10-06 16:36:34 +020054
55@interface NTViewController ()
56
57@property (nonatomic, strong) CLLocationManager *locationManager;
58@property (nonatomic, strong) CLBeaconRegion *beaconRegion;
59@property (nonatomic, strong) CBPeripheralManager *peripheralManager;
Nick Toumpelis6d4ea722013-10-06 21:45:25 +020060@property (nonatomic, strong) NSArray *detectedBeacons;
Nick Toumpelis0c019102013-10-20 22:28:05 +020061@property (nonatomic, weak) UISwitch *advertisingSwitch;
62@property (nonatomic, weak) UISwitch *rangingSwitch;
Nick Toumpelis02e5ae62013-10-06 16:36:34 +020063
64@end
65
66@implementation NTViewController
67
Nick Toumpelis02e5ae62013-10-06 16:36:34 +020068#pragma mark - Beacon ranging
69- (void)createBeaconRegion
70{
71 if (self.beaconRegion)
72 return;
73
74 NSUUID *proximityUUID = [[NSUUID alloc] initWithUUIDString:kUUID];
75 self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:proximityUUID identifier:kIdentifier];
76}
77
78- (void)turnOnRanging
79{
80 NSLog(@"Turning on ranging...");
81
82 if (![CLLocationManager isRangingAvailable]) {
83 NSLog(@"Couldn't turn on ranging: Ranging is not available.");
84 self.rangingSwitch.on = NO;
85 return;
86 }
87
88 if (self.locationManager.rangedRegions.count > 0) {
89 NSLog(@"Didn't turn on ranging: Ranging already on.");
90 return;
91 }
92
93 [self createBeaconRegion];
94 [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
95
96 NSLog(@"Ranging turned on for region: %@.", self.beaconRegion);
97}
98
99- (void)changeRangingState:sender
100{
101 UISwitch *theSwitch = (UISwitch *)sender;
102 if (theSwitch.on) {
103 [self startRangingForBeacons];
104 } else {
105 [self stopRangingForBeacons];
106 }
107}
108
109- (void)startRangingForBeacons
110{
111 self.locationManager = [[CLLocationManager alloc] init];
112 self.locationManager.delegate = self;
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200113
Nick Toumpelis0c019102013-10-20 22:28:05 +0200114 self.detectedBeacons = [NSArray array];
115
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200116 [self turnOnRanging];
117}
118
119- (void)stopRangingForBeacons
120{
121 if (self.locationManager.rangedRegions.count == 0) {
122 NSLog(@"Didn't turn off ranging: Ranging already off.");
123 return;
124 }
125
126 [self.locationManager stopRangingBeaconsInRegion:self.beaconRegion];
Nick Toumpelis0c019102013-10-20 22:28:05 +0200127
128 NSIndexSet *deletedSections = [self deletedSections];
129 self.detectedBeacons = [NSArray array];
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200130
Nick Toumpelis0c019102013-10-20 22:28:05 +0200131 [self.beaconTableView beginUpdates];
132 if (deletedSections)
133 [self.beaconTableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationFade];
134 [self.beaconTableView endUpdates];
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200135
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200136 NSLog(@"Turned off ranging.");
137}
138
Nick Toumpelis0c019102013-10-20 22:28:05 +0200139#pragma mark - Index path management
140- (NSArray *)indexPathsOfRemovedBeacons:(NSArray *)beacons
141{
142 NSMutableArray *indexPaths = nil;
143
144 NSUInteger row = 0;
145 for (CLBeacon *existingBeacon in self.detectedBeacons) {
146 BOOL stillExists = NO;
147 for (CLBeacon *beacon in beacons) {
148 if ((existingBeacon.major.integerValue == beacon.major.integerValue) &&
149 (existingBeacon.minor.integerValue == beacon.minor.integerValue)) {
150 stillExists = YES;
151 break;
152 }
153 }
154 if (!stillExists) {
155 if (!indexPaths)
156 indexPaths = [NSMutableArray new];
157 [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
158 }
159 row++;
160 }
161
162 return indexPaths;
163}
164
165- (NSArray *)indexPathsOfInsertedBeacons:(NSArray *)beacons
166{
167 NSMutableArray *indexPaths = nil;
168
169 NSUInteger row = 0;
170 for (CLBeacon *beacon in beacons) {
171 BOOL isNewBeacon = YES;
172 for (CLBeacon *existingBeacon in self.detectedBeacons) {
173 if ((existingBeacon.major.integerValue == beacon.major.integerValue) &&
174 (existingBeacon.minor.integerValue == beacon.minor.integerValue)) {
175 isNewBeacon = NO;
176 break;
177 }
178 }
179 if (isNewBeacon) {
180 if (!indexPaths)
181 indexPaths = [NSMutableArray new];
182 [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
183 }
184 row++;
185 }
186
187 return indexPaths;
188}
189
190- (NSArray *)indexPathsForBeacons:(NSArray *)beacons
191{
192 NSMutableArray *indexPaths = [NSMutableArray new];
193 for (NSUInteger row = 0; row < beacons.count; row++) {
194 [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:NTDetectedBeaconsSection]];
195 }
196
197 return indexPaths;
198}
199
200- (NSIndexSet *)insertedSections
201{
202 if (self.rangingSwitch.on && [self.beaconTableView numberOfSections] == kNumberOfSections - 1) {
203 return [NSIndexSet indexSetWithIndex:1];
204 } else {
205 return nil;
206 }
207}
208
209- (NSIndexSet *)deletedSections
210{
211 if (!self.rangingSwitch.on && [self.beaconTableView numberOfSections] == kNumberOfSections) {
212 return [NSIndexSet indexSetWithIndex:1];
213 } else {
214 return nil;
215 }
216}
217
218- (NSArray *)filteredBeacons:(NSArray *)beacons
219{
220 // Filters duplicate beacons out; this may happen temporarily if the originating device changes its Bluetooth id
221 NSMutableArray *mutableBeacons = [beacons mutableCopy];
222
223 NSMutableSet *lookup = [[NSMutableSet alloc] init];
224 for (int index = 0; index < [beacons count]; index++) {
225 CLBeacon *curr = [beacons objectAtIndex:index];
226 NSString *identifier = [NSString stringWithFormat:@"%@/%@", curr.major, curr.minor];
227
228 // this is very fast constant time lookup in a hash table
229 if ([lookup containsObject:identifier]) {
230 [mutableBeacons removeObjectAtIndex:index];
231 } else {
232 [lookup addObject:identifier];
233 }
234 }
235
236 return [mutableBeacons copy];
237}
238
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200239#pragma mark - Beacon ranging delegate methods
240- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
241{
242 if (![CLLocationManager locationServicesEnabled]) {
243 NSLog(@"Couldn't turn on ranging: Location services are not enabled.");
244 self.rangingSwitch.on = NO;
245 return;
246 }
Nick Toumpelis0c019102013-10-20 22:28:05 +0200247
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200248 if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized) {
249 NSLog(@"Couldn't turn on ranging: Location services not authorised.");
250 self.rangingSwitch.on = NO;
251 return;
252 }
253
254 self.rangingSwitch.on = YES;
255}
256
257- (void)locationManager:(CLLocationManager *)manager
258 didRangeBeacons:(NSArray *)beacons
259 inRegion:(CLBeaconRegion *)region {
Nick Toumpelis0c019102013-10-20 22:28:05 +0200260 NSArray *filteredBeacons = [self filteredBeacons:beacons];
261
262 if (filteredBeacons.count == 0) {
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200263 NSLog(@"No beacons found nearby.");
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200264 } else {
Nick Toumpelis0c019102013-10-20 22:28:05 +0200265 NSLog(@"Found %lu %@.", (unsigned long)[filteredBeacons count],
266 [filteredBeacons count] > 1 ? @"beacons" : @"beacon");
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200267 }
268
Nick Toumpelis0c019102013-10-20 22:28:05 +0200269 NSIndexSet *insertedSections = [self insertedSections];
270 NSIndexSet *deletedSections = [self deletedSections];
271 NSArray *deletedRows = [self indexPathsOfRemovedBeacons:filteredBeacons];
272 NSArray *insertedRows = [self indexPathsOfInsertedBeacons:filteredBeacons];
273 NSArray *reloadedRows = nil;
274 if (!deletedRows && !insertedRows)
275 reloadedRows = [self indexPathsForBeacons:filteredBeacons];
276
277 self.detectedBeacons = filteredBeacons;
278
279 [self.beaconTableView beginUpdates];
280 if (insertedSections)
281 [self.beaconTableView insertSections:insertedSections withRowAnimation:UITableViewRowAnimationFade];
282 if (deletedSections)
283 [self.beaconTableView deleteSections:deletedSections withRowAnimation:UITableViewRowAnimationFade];
284 if (insertedRows)
285 [self.beaconTableView insertRowsAtIndexPaths:insertedRows withRowAnimation:UITableViewRowAnimationFade];
286 if (deletedRows)
287 [self.beaconTableView deleteRowsAtIndexPaths:deletedRows withRowAnimation:UITableViewRowAnimationFade];
288 if (reloadedRows)
289 [self.beaconTableView reloadRowsAtIndexPaths:reloadedRows withRowAnimation:UITableViewRowAnimationNone];
290 [self.beaconTableView endUpdates];
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200291}
292
293#pragma mark - Beacon advertising
294- (void)turnOnAdvertising
295{
Nick Toumpelisb2b3f202013-10-20 13:31:00 +0200296 if (self.peripheralManager.state != CBPeripheralManagerStatePoweredOn) {
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200297 NSLog(@"Peripheral manager is off.");
298 self.advertisingSwitch.on = NO;
299 return;
300 }
301
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200302 time_t t;
303 srand((unsigned) time(&t));
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200304 CLBeaconRegion *region = [[CLBeaconRegion alloc] initWithProximityUUID:self.beaconRegion.proximityUUID
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200305 major:rand()
306 minor:rand()
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200307 identifier:self.beaconRegion.identifier];
308 NSDictionary *beaconPeripheralData = [region peripheralDataWithMeasuredPower:nil];
309 [self.peripheralManager startAdvertising:beaconPeripheralData];
Nick Toumpelisb2b3f202013-10-20 13:31:00 +0200310
311 NSLog(@"Turning on advertising for region: %@.", region);
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200312}
313
314- (void)changeAdvertisingState:sender
315{
316 UISwitch *theSwitch = (UISwitch *)sender;
317 if (theSwitch.on) {
318 [self startAdvertisingBeacon];
319 } else {
320 [self stopAdvertisingBeacon];
321 }
322}
323
324- (void)startAdvertisingBeacon
325{
326 NSLog(@"Turning on advertising...");
327
328 [self createBeaconRegion];
329
330 if (!self.peripheralManager)
331 self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
332
333 [self turnOnAdvertising];
334}
335
336- (void)stopAdvertisingBeacon
337{
338 [self.peripheralManager stopAdvertising];
339
340 NSLog(@"Turned off advertising.");
341}
342
343#pragma mark - Beacon advertising delegate methods
344- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheralManager error:(NSError *)error
345{
346 if (error) {
347 NSLog(@"Couldn't turn on advertising: %@", error);
348 self.advertisingSwitch.on = NO;
349 return;
350 }
351
352 if (peripheralManager.isAdvertising) {
353 NSLog(@"Turned on advertising.");
354 self.advertisingSwitch.on = YES;
355 }
356}
357
358- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheralManager
359{
Nick Toumpelisb2b3f202013-10-20 13:31:00 +0200360 if (peripheralManager.state != CBPeripheralManagerStatePoweredOn) {
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200361 NSLog(@"Peripheral manager is off.");
362 self.advertisingSwitch.on = NO;
363 return;
364 }
365
366 NSLog(@"Peripheral manager is on.");
367 [self turnOnAdvertising];
368}
369
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200370#pragma mark - Table view functionality
Nick Toumpelis0c019102013-10-20 22:28:05 +0200371- (NSString *)detailsStringForBeacon:(CLBeacon *)beacon
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200372{
Nick Toumpelis0c019102013-10-20 22:28:05 +0200373 NSString *proximity;
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200374 switch (beacon.proximity) {
375 case CLProximityNear:
Nick Toumpelis0c019102013-10-20 22:28:05 +0200376 proximity = @"Near";
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200377 break;
378 case CLProximityImmediate:
Nick Toumpelis0c019102013-10-20 22:28:05 +0200379 proximity = @"Immediate";
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200380 break;
381 case CLProximityFar:
Nick Toumpelis0c019102013-10-20 22:28:05 +0200382 proximity = @"Far";
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200383 break;
384 case CLProximityUnknown:
385 default:
Nick Toumpelis0c019102013-10-20 22:28:05 +0200386 proximity = @"Unknown";
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200387 break;
Nick Toumpelis142771e2013-10-06 19:09:24 +0200388 }
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200389
Nick Toumpelis0c019102013-10-20 22:28:05 +0200390 NSString *format = @"%@, %@ • %@ • %f • %li";
391 return [NSString stringWithFormat:format, beacon.major, beacon.minor, proximity, beacon.accuracy, beacon.rssi];
392}
393
394- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
395{
396 UITableViewCell *cell = nil;
397 switch (indexPath.section) {
398 case NTOperationsSection: {
399 cell = [tableView dequeueReusableCellWithIdentifier:kOperationCellIdentifier];
400 switch (indexPath.row) {
401 case NTAdvertisingRow:
402 cell.textLabel.text = kAdvertisingOperationTitle;
403 self.advertisingSwitch = (UISwitch *)cell.accessoryView;
404 [self.advertisingSwitch addTarget:self
405 action:@selector(changeAdvertisingState:)
406 forControlEvents:UIControlEventValueChanged];
407 break;
408 case NTRangingRow:
409 default:
410 cell.textLabel.text = kRangingOperationTitle;
411 self.rangingSwitch = (UISwitch *)cell.accessoryView;
412 [self.rangingSwitch addTarget:self
413 action:@selector(changeRangingState:)
414 forControlEvents:UIControlEventValueChanged];
415 break;
416 }
417 }
418 break;
419 case NTDetectedBeaconsSection:
420 default: {
421 CLBeacon *beacon = self.detectedBeacons[indexPath.row];
422
423 cell = [tableView dequeueReusableCellWithIdentifier:kBeaconCellIdentifier];
424
425 if (!cell)
426 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
427 reuseIdentifier:kBeaconCellIdentifier];
428
429 cell.textLabel.text = beacon.proximityUUID.UUIDString;
430 cell.detailTextLabel.text = [self detailsStringForBeacon:beacon];
431 cell.detailTextLabel.textColor = [UIColor grayColor];
432 }
433 break;
434 }
435
436 return cell;
Nick Toumpelis142771e2013-10-06 19:09:24 +0200437}
438
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200439- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
440{
Nick Toumpelis0c019102013-10-20 22:28:05 +0200441 if (self.rangingSwitch.on) {
442 return kNumberOfSections; // All sections visible
443 } else {
444 return kNumberOfSections - 1; // Beacons section not visible
445 }
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200446}
447
448- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
449{
Nick Toumpelis0c019102013-10-20 22:28:05 +0200450 switch (section) {
451 case NTOperationsSection:
452 return kNumberOfAvailableOperations;
453 case NTDetectedBeaconsSection:
454 default:
455 return self.detectedBeacons.count;
456 }
Nick Toumpelis6d4ea722013-10-06 21:45:25 +0200457}
458
459- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
460{
Nick Toumpelis0c019102013-10-20 22:28:05 +0200461 switch (section) {
462 case NTOperationsSection:
463 return nil;
464 case NTDetectedBeaconsSection:
465 default:
466 return kBeaconSectionTitle;
467 }
468}
469
470- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
471{
472 switch (indexPath.section) {
473 case NTOperationsSection:
474 return kOperationCellHeight;
475 case NTDetectedBeaconsSection:
476 default:
477 return kBeaconCellHeight;
478 }
479}
480
481- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
482{
483 UITableViewHeaderFooterView *headerView =
484 [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:kBeaconsHeaderViewIdentifier];
485
486 // Adds an activity indicator view to the section header
487 UIActivityIndicatorView *indicatorView =
488 [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
489 [headerView addSubview:indicatorView];
490
491 indicatorView.frame = (CGRect){kActivityIndicatorPosition, indicatorView.frame.size};
492
493 [indicatorView startAnimating];
494
495 return headerView;
Nick Toumpelis142771e2013-10-06 19:09:24 +0200496}
497
Nick Toumpelis02e5ae62013-10-06 16:36:34 +0200498@end