Bluetooth, iOS, Objective-C

Breaking Down Bluetooth in Objective-C and iOS7

bluetoothMy last post After the Install: Scenario-Based RVM Best Practices was over a month ago. Since then, I’ve been immersed in an iOS bootcamp. Today’s post has been my first chance to come up for air and share a bit about what I’ve learned.

Similar to Rails, my journey into iOS has come with a lot of support from other devs. In fact, this post was made possible by my bootcamp instructors John Bender and John Clem as well as one of my classmates – Christian Hansen. Specifically, John and John wrote 80% of the code that appears in this post. Christian and I worked closely together to optimize the code and write a thorough explanation of its inner workings. I hope you enjoy the results of our teamwork.

APPLICATION OVERVIEW
For today’s post, Christian and I will walk you through an iPhone application that leverages Apple’s Multipeer Connectivity Framework. Specifically, the application does the following:

  • Has a view showing three colored shapes – called “bubbles”.
  • When another device running the same application is in range, a Bluetooth connection is enabled.
  • When either user drags a bubble with their finger, other connected devices show the same movement.

icon@58Note! I recently released an iPhone app and would appreciate it if you would give it a try. It’s free and called smoov. smoov is a text messaging assistant for gents.

You can download it here or search for it by name in the App Store.


From an architectural standpoint, the application has the following 3 main components, each of which will be thoroughly explained in this post.

    1. View Controller
    2. Bubble View
    3. Bluetooth Manager

Before we get into any code, here’s an overview of the major responsibilities of each of the main components.

bluetooth_arch

The project does include some other components, that while essential for the application to work properly, won’t be addressed in detail in this post. Specifically, we won’t address the contents of the Supporting Files folder, the App Delegate files, the Tests folder, or the Products folder.

Additionally, the astute reader will notice files titled UIColor+RandomColor in the project. These files represent an Objective-C category that make it easy to programmatically assign random colors to UIViews. This file will not get a line-by-line explanation, but it’s role and purpose will be referenced briefly in the Bubble View section.

RUNNING THE APPLICATION LOCALLY
As you read this post, you might find it useful to run the application locally to see it do its thing (as well as inspect the NSLog output).  To do so, simply drop the below into your command line.

$ git clone git@github.com:mxstrand/iOS_Bluetooth_App.git

Then open the project in Xcode and deploy.

Note: This application will successfully pair between a true iOS device (iPhone or iPod touch) as well as the Xcode simulator. Just deploy one right after the other and wait for the pairing confirmation.

With all that out of the way, let’s walk through what’s happening in each of the 3 main components.

1. VIEW CONTROLLER
After the App Delegate files, the View Controller is the first to fire when the app starts up. As such, the BTViewController.h file will be a good place to start our code review. There’s not much happening here, but I will call out that because our BTViewController inherits from UIViewController, the UIKit is imported automatically.

#import <UIKit/UIKit.h>

@interface BTViewController : UIViewController

@end

Moving onto the BTViewController.m file we see a bit more going on. First let’s talk about the imports:

#import "BTViewController.h"
#import "BTBubbleView.h"
#import "BTBluetoothManager.h"

Here we are importing the header file of the same name.  Additionally, we import the header files of the other two key components of the app (as outlined in the Application Overview section above). This is necessary as we’ll be sending and/or receiving information with both the BTBubbleView and BTBluetoothManager. Both the interaction between and the inner workings of these files will be fully addressed in the course of this post.

Next we define some constants that will be fundamental building blocks for our application. As mentioned, our app shows “bubbles” on the screen. With the below constants we begin to define the count and size of bubbles to be used.

static const NSInteger nBubbles = 3;
static const CGFloat bubbleSize = 50.;

Another cornerstone of this view controller is establishing that we’ll be storing references to our bubbles in an array as per the below.

@interface BTViewController () { NSArray *bubbles; } @end

As you’ll soon see, storing references to the bubbles in an array makes it easy for us to manage all the bubbles at once when necessary.

Moving into the @implementation BTViewController section, we first define some initial instructions to run immediately after the view appears. If you are newer to Objective-C, I’ll call out here that viewDidLoad is a pre-existing function that is quite common is iOS view controllers as it runs automatically after a view appears on screen. In fact, viewDidLoad is just one of a series of useful pre-existing functions to automatically fire based the view rendering timeline. For a list of other similar functions, check out this list of instance methods from Apple’s reference documentation on the UIViewController.

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self makeBubbles];
    [BTBluetoothManager instance];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(bluetoothDataReceived:)
                                                 name:@"bluetoothDataReceived"
                                               object:nil];
}

Breaking down the above, first we inform the parent controller (the UI View controller) that after it’s done doing its viewDidLoad thing, we’ll be providing our own set of instructions.  As such, our first request is to run a method called makeBubbles that is defined in the same file. While this post will cover exactly what makeBubbles does very soon, let’s finish with the rest of viewDidLoad. Next we run a class method defined in the BTBluetoothManager called instance. This too will be addressed in detail, but for now I’ll just say that gets our Bluetooth engine running. Similarly, we tell NSNotificationCenter to begin watching for a specific event – namely an event named bluetoothDataReceived. Additionally, when that event occurs, please send a message to the View Controller. Conceptually, the NSNotificationCenter is like a dispatcher or telephone operator. It listens for certain events, and upon receipt, broadcasts subsequent messages or instructions.

Okay, now for some fun! Let’s make bubbles! As mention, the below method is called during viewDidLoad. Have a read and then let’s discuss what it’s doing.

-(void) makeBubbles { NSMutableArray *b = [NSMutableArray new]; for( NSInteger i = 0; i < nBubbles; i++ ) { BTBubbleView *bubble = [[BTBubbleView alloc] initWithFrame:CGRectMake( bubbleSize, bubbleSize*i, bubbleSize*i, bubbleSize*i )]; bubble.originalIndex = i; NSLog (@"Bubble was given index# %d", bubble.originalIndex); [self.view addSubview:bubble]; [b addObject:bubble]; } bubbles = [NSArray arrayWithArray:b]; }

First we instantiate a mutable array. A mutable array is simply an array that can be altered, meaning values can both be added or removed from it. We also assign a temporary pointer (“b” for bubbles) to this array’s location in memory.

With our empty array in place, we trigger a standard for loop that will run until our counter i is no longer less than the constant for bubble count (nBubbles) that we defined at the top of this file. In summary we’re saying we want this loop to run three times.

The first step of the loop is to create a new instance of the BTBubbleView class (we can do this because of the #import "BTBubbleView.h" declaration at the top of the file). Thankfully, Objective-C gives us the initWithFrame:CGRectMake statement that basically draws a rectangle in the view. This statements takes 4 main arguments – x, y coordinates for location on the screen and width and height in pixel size.

After creation, we assign a value to a property called originalIndex. We’ll be using this value later to signal to other devices paired with us via Bluetooth which of the bubbles is moving.

Next we see our first NSLog. NSLog doesn’t impact anything in the user experience, it simply  instructs the application to write a message to the log file – visible to us app developers during runtime, when the application runs while connected to XCode. This is a common debug technique and here we are writing a log statement when each bubble is created and what originalIndex value the bubble has been given. You’ll see quite a few more NSLog statements throughout the code.

Now that a bubble has been created, we use the predefined method addSubview to add it to our main View Controller.

Next, we use the pre-defined method addObject to add our bubble to the end of our mutable array called “b”.

Great, so that’s one bubble created, given an identifier, added to the view, and stored in an array. From here the for loop runs twice more, creating a second and third bubble.

If you look closely, you’ll see that “i’ is influences bubble location and size. As such, our second bubble is placed in a different position on the screen and it slightly larger than the first bubble created.

After the for loop finishes, we use the arrayWithArray method to move our bubbles from a mutable array called b into a non-mutable array that we call bubbles. We’ll be using the bubbles array in the next method.

The next function -(void) bluetoothDataReceived:(NSNotification*)note is a cornerstone of the application. In summary, it receives a “note” from the NSNotificationCenter. This should sound familiar as we discussed notifications just a few paragraphs above. The key point was

Conceptually, the NSNotificationCenter is like a dispatcher or telephone operator. It listens for certain events, and upon receipt, broadcasts subsequent messages or instructions.

In this case specifically, the View Controller has been passed a note of bluetoothDataReceived from the NSNotificationCenter. As the name implies, this method will instruct the View Controller how to update itself based on the receipt of Bluetooth data.

Looking more closely, let’s break down the first few lines of the method.

-(void) bluetoothDataReceived:(NSNotification*)note { [[NSOperationQueue mainQueue] addOperationWithBlock:^{ NSDictionary *dict = [note object]; NSInteger command = [dict[@"command"] intValue];

If your background is in Rails (like mine), you may not be used to instructing your application to use multiple processing queues of varying priority. However, the first line here is basically putting this method front-and-center on the main processing queue. While such instructions should be used thoughtfully, given that we want near-real-time mirroring of bubble movement between the paired devices, this is an appropriate use of the ability.

You might be asking yourself, “is queuing the same as multi-threading?”. The answer is mostly yes. You can think NSOperationQueue as an abstracted, higher-level approach to multithreading. As it’s outside of the scope of this post, I’ll offer the following StackOverflow posts as a good starting point if you’d like to scratch the surface of the nuances.

Next we instantiate an NSDictionary object, and we base that dictionary on the “note” received from the NSNotificationCenter.

After that we’ll pull the “command” out of the dictionary.  In our application, there are three basic commands that can be passed between devices. In summary, they are:

  1. An object has been picked up (started moving)
  2. An object is moving (this includes x,y coordinates throughout it’s movement path)
  3. An object has been set down (stopped moving).

Knowing this, the following switch statement will begin to make sense.

switch( command ) { case BluetoothCommandPickUp: { NSInteger viewNumber = [dict[@"viewNumber"] intValue]; NSLog (@"PICKED UP with ARRAY-based viewNumber %ld", (long)viewNumber); for( BTBubbleView *bubble in bubbles ) if( bubble.originalIndex == viewNumber ) { [bubble performSelectorOnMainThread:@selector(pickUp) withObject:nil waitUntilDone:YES];     break; } } case BluetoothCommandDrop: { NSInteger viewNumber = [dict[@"viewNumber"] intValue]; NSLog (@"DROPPED with ARRAY-based viewNumber %ld", (long)viewNumber); BTBubbleView *bubble; for( NSInteger i = 0; i < bubbles.count; i++ ) { bubble = bubbles[i]; if( bubble.originalIndex == viewNumber ) { break; } } if( [bubble isKindOfClass:[BTBubbleView class]] ) [bubble performSelectorOnMainThread:@selector(drop) withObject:nil waitUntilDone:YES]; break; } case BluetoothCommandMove: { NSInteger viewNumber = [dict[@"viewNumber"] intValue]; for( BTBubbleView *bubble in bubbles ) if( bubble.originalIndex == viewNumber ) { bubble.center = [dict[@"newCenter"] CGPointValue]; [bubble performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:nil waitUntilDone:YES]; break; } } } }];

As we have 3 basic commands being shared via Bluetooth, we have 3 cases being evaluated in our switch statement. Based on the command (pick up, move, or drop) found in the dictionary received, the method then goes on to verify which bubble the command is intended for.

You’ll note we’ve embedded some NSLogs here as well which make for some interesting reading if you monitor the console in Xcode while moving the bubbles on paired devices.

Now that the case condition knows which command should be run and which bubble it should be run against, we again see a priority execution statement of performSelectorOnMainThread (arguably redundant based on the previous NSOperationQueue mainQueue) .

The @selector argument is how the receiving device knows what is needs to do. In the cases of pick up and drop, is refers to methods of the same name in the BubbleViewController files.

In the case of move, the bubble repositions its center to new x,y coordinates that have been passed along in the dictionary as per CGPointValue.

Like I said, the above method is a cornerstone of the application and really worth taking some time to understand. Between the use of NSNotification, NSOperationalQueue, switch cases, for loops, dictionaries, and CGPointValue it’s a great piece of code to learn from to take your understanding of Objective-C to a new level.

To close out this file, we’ve got another very common, pre-existing method.

- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. }

As per the method name and comment, it’s easy to infer that when the application experiences a low memory warning , this method will trigger the release of available memory. It you’d like to read more about this function, here’s the link.

2. BUBBLE VIEW
Taking a look at BTBubbleView.h, we find the following:

#import <UIKit/UIKit.h>

Similar to our BTViewController,  our BubbleView inherits from UIView, thus UIKit is imported automatically when the file was created.

Next, we tee up the fact that we want to be able to assign our BubbleView objects a numerical property called originalIndex. If you look back, that we’ve already seen this property in use in our bluetoothDataReceived function. While this value is interpreted (think “getter”) in that method, we’ll see it being set (think “setter”) in shortly.

@property (nonatomic, assign) NSInteger originalIndex;

Next, we give a heads up that our implementation file (.m) will include both pickUp and drop methods. These too we saw used in our bluetoothDataReceived function.

-(void) pickUp; -(void) drop;

Keep reading to see exactly what these methods do.

Moving along to BubbleView.m, we see a number of import statements.

#import "BTBubbleView.h" #import "BTBluetoothManager.h" #import "UIColor+RandomColor.h" #import <QuartzCore/QuartzCore.h>

The first 2 imports should look familiar as they are the other 2 “main components” of the application described in the introduction. Specifically, the BTBubbleView.h is the header of the file set we just finished explaining in detail. The BTBluetoothManager.h is the header of the file set we’ll explain in the next section.

The UIColor+RandomColor is a basic example of an Objective-C category. This one in particular, provides some helper methods to easily format UIView objects with random colors. If you want to see exactly how that’s done check out the source code – it’s only a few lines.

QuartzCore is a framework we’ll borrow some methods from to format our bubbles. More on that next.

While our header (.h) file outlined a few properties and methods publicly so they could be leveraged by the BTViewController, our implementation (.m) file defines some properties and methods that will be used privately only. Namely,

@interface BTBubbleView () { BOOL isMoving; UITouch *movingTouch; CGSize touchOffset; CGPoint originalPosition; } @end

While the names are quite descriptive, each of these lines of code will be fully explained as we move through the rest of this file. Let’s start with the first method in the @implementation section.

-(id) initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if( self ) { self.backgroundColor = [UIColor randomColor]; } return self; }

initWithFrame is a pre-existing UIView method that runs whenever a BubbleView object is created. Here we also set its color using the randomColor method. This ability is coming from the before-mentioned UIColor+RandomColor category. Because we’ve imported the category into our file, we can call it here to set our bubble background color. If you are wondering where the bubble size is being set, have a look back at the makeBubbles function in the BTViewController.m. To understand the full chain of events, when the main View Controller’s viewDidLoad action fires, it calls makeBubbles, which in turn calls initWithFrame.

Next, we are going to look at bubble movement and sending notification of that movement to Bluetooth.

First we have touchesBegan. touchesBegan is a pre-existing UIEvent class function and from Apple’s own documentation it says, “Tells the receiver when one or more fingers touch down in a view or window”.

-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event //touchesBegan is a canned function. { if( isMoving ) return; //if an object is already moving, end function. movingTouch = [touches anyObject]; CGPoint touchPoint = [movingTouch locationInView:self.superview]; touchOffset = CGSizeMake( self.center.x - touchPoint.x, self.center.y - touchPoint.y ); originalPosition = self.center; [self pickUp]; // Create a dictionary containing key, value pairs for an indicator of the current command and the acting object. Send that dictionary to your peers via the BluetoothManager. NSDictionary *dict = @{@"command": @(BluetoothCommandPickUp), @"viewNumber": @(_originalIndex)}; [[BTBluetoothManager instance] sendDictionaryToPeers:dict]; }

As you can see, the first check we is whether or not the touched object is already moving. If it is, no further action is taken. We accomplish this a simple check using the isMoving Boolean. By default, isMoving is set to FALSE – so the first time it is evaluated, the function continues as desired.

Next we put the movingTouch object to work. As declared at the beginning of the file, the movingTouch object inherits from the UITouch class.  Basically, the touchesBegan function allows us to identify which object has been touched. As you can see the touchesBegan function takes an NSSet as an argument. This means it can interpret more than one touch (finger) on a single view. While this is useful for multi-touch scenarios like 2 finger pinch and zoom, it’s more ability than we need.  In our case, we simply evaluate the NSSet and grab the first touch – it is this touch that consider our movingTouch for further evaluation. Next, we note exactly where on the movingTouch’s location the user touched – we do this by recording its x, y cooridnates within the superview (the view within BTViewController). By comparing the x, y coordinates of where the bubble was touched with its center, we calculate what we call the touchOffset. We’ll use the touchOffset in the subsequent touchesMoved function to creates a handle-like behavior – more on that shortly.

Next, we record the starting point of the bubble and pass it into a variable called originalPosition, before calling a custom method called pickUp.

The final two lines of touchesBegan create a dictionary of  key information to be passed to paired devices. Finally, the dictionary is send to the Bluetooth Mananger.

Moving on, let’s take a closer look at what the pickUp fuction does for us.

-(void) pickUp { isMoving = TRUE; [UIView animateWithDuration:0.3 animations:^{ self.alpha = 0.85; CGAffineTransform t = CGAffineTransformMakeScale( 1.11, 1.11 ); t = CGAffineTransformRotate( t, M_PI ); self.transform = t; self.layer.cornerRadius = 50.; } completion:^(BOOL finished) { }]; }

First of all, it flips the isMoving Boolean to TRUE, so that if a paired device tries to move the same bubble concurrently it will not respond. (This should raise an interesting question in your mind about resolution of near-simultaneous, conflicting movement of bubbles. This topic is addressed in the Bluetooth Manager section which comes next).

Next, we use Objective-C’s animateWithDuration command to add some special events to the touchesBegan event. In our case, we give the touched object a bit of transparency, make it 10% larger, spin it, and round its corners.

We’ve added a cool effect to when the object is touched and passed some information about the touched object to paired devices via Bluetooth, next we handle movement or dragging of the object.

-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event // touchesMoved is a canned function. { if( [touches containsObject:movingTouch] ) { CGPoint touchPoint = [movingTouch locationInView:self.superview]; self.center = CGPointMake( touchPoint.x + touchOffset.width, touchPoint.y + touchOffset.height ); // Create a dictionary containing key, value pairs for an indicator of the current command and the acting object. Additionally, this dictionary will contain the x, y coordinates of the bubble's center as it moves. NSDictionary *dict = @{@"command": @(BluetoothCommandMove), @"viewNumber": @(_originalIndex), @"newCenter": [NSValue valueWithCGPoint:self.center]}; [[BTBluetoothManager instance] sendDictionaryToPeers:dict]; } }

Just like touchesBegan, touchesMoved is a pre-existing UIEvent class function. And just like touchesBegan the main purpose of this function is to record key information about movement (via a Dictionary) and send that information to the Bluetooth Manager. As discussed before however, this method also leverages the previously calculated touchesOffset to actually shift the center of the moving object away by a fixed amount – creating a handle effect.

One other key difference between touchesBegan and touchesMoved is what is passed in the dictionary data set. Besides an identifier for which bubble is moving as well as a command identifier (pickup, move, or drop), the move dictionary include a newCenter value. This is effectively instruction on where a paired device should move the bubble to on its view to mirror the movement on the original device.

At this point, a detailed explanation of touchesEnded is probably overkill.

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event // touchesEnded is a canned function. { if( [touches containsObject:movingTouch] ) { [self drop]; movingTouch = nil; // Create a dictionary containing key, value pairs for an indicator of the current command and the acting object. Send that dictionary to your peers via the BluetoothManager. NSDictionary *dict = @{@"command": @(BluetoothCommandDrop), @"viewNumber": @(_originalIndex)}; [[BTBluetoothManager instance] sendDictionaryToPeers:dict]; } }

That said, it’s probably noteworthy to call out that here we set the movingTouch object back to nil and call a custom function called drop. In summary, drop is similar to pickUp in that it includes some custom animations as well as sets our isMoving Boolean back to FALSE.

-(void) drop { [UIView animateWithDuration:0.3 animations:^{ self.alpha = 1.; // remove transparency self.transform = CGAffineTransformIdentity; self.layer.cornerRadius = 0.1; // round corners } completion:^(BOOL finished) { self.layer.cornerRadius = 0.; // remove rounded corners }]; isMoving = FALSE; // by setting back to false, the touchesBegan function will run fully when triggered. }

Next, we’ve got touchesCancelled. touchesCancelled is invoked if the user does something like drag their finger off the screen. In this case, the previous moves are undone by placing the bubble back at it’s original position – which was recorded in touchesBegan. It’s effectively an undo.

-(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event // touchesCancelled is a canned function. { if( [touches containsObject:movingTouch] ) { [UIView animateWithDuration:0.3 animations:^{ self.center = originalPosition; } completion:^(BOOL finished) { [self touchesEnded:touches withEvent:event]; }]; } }

That’s it for the Bubble View, now let’s get into the Bluetooth Manager!

3. BLUETOOTH MANAGER
Up to this point we have looked at the BTViewController and the BTBubbleView classes which were laying the groundwork for how our app will function, display and pass data between peers Now though, we will look at how the sessions are created and managed within the BTBluetoothManager class.

For our app to work, we imported the MultipeerConnectivity.framework into the project. If you’re unfamiliar with adding frameworks, jump over to Apple’s documentation here.

So let’s jump right into the BluetoothManager.h file

#import <Foundation/Foundation.h> #import <MultipeerConnectivity/MultipeerConnectivity.h>

Here we are importing the framework that we previously included in our project.

The second chunk of code here is the typdef enum block which assigns a number value to the commands listed. The benefit of assigning values to strings gives you (the programmer) a more human-readable way to pass commands and it gives Xcode (the compiler) a way to simply pass a number instead of a string (computers dig numbers).

typedef enum { BluetoothCommandHandshake=1, BluetoothCommandNegotiate, BluetoothCommandNegotiateConfirm, BluetoothCommandLayout, BluetoothCommandPickUp, BluetoothCommandMove, BluetoothCommandDrop } BluetoothCommand;

Now we are adopting the delegate protocols to our class. Essentially this just mean that while this class inherits from NSObject it is also implements (adopts) the three delegates within the <> brackets. The next parts, within the curly braces, we are declaring our instance variables for nearbyBrowser, nearbyAdvertiser, session, peerId and playerIndexTimestamp which will all be used later in the code.

@interface BTBluetoothManager : NSObject <MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate, MCSessionDelegate> { MCNearbyServiceBrowser *nearbyBrowser; MCNearbyServiceAdvertiser *nearbyAdvertiser; MCSession *session; NSString *peerId; NSDate *playerIndexTimestamp; }

The last part in BTBluetoothManager.h is the code below. Here we are creating the NSString property peerName” and the NSInteger property playerIndex. By defining these properties as nonatomic and readonly, we are stating that properties are not thread-safe and that they cannot be changed.

@property (nonatomic, readonly) NSString *peerName; @property (nonatomic, readonly) NSInteger playerIndex;

Then we have two class methods and also one instance method being declared that we will be looking at further in the implementation file.

+(BTBluetoothManager*) instance; +(BOOL) hasConnection; -(void) sendDictionaryToPeers:(NSDictionary*)dict;

Now we are done with the header file and will go through the implementation file. First we import the BTBluetoothManager header file and just below that we are simply setting a constant for the BTAppId. Setting constants is a good idea because it ensures a simple typo won’t leave you complete baffled and searching through all of your code for one tiny mistake. Now, we can use the kBTAppID alias any time we actually mean @bluetoofdemo.

#import "BTBluetoothManager.h" #define kBTAppID @"bluetoofdemo" static BTBluetoothManager *sharedInstance = nil; @implementation BTBluetoothManager +(BTBluetoothManager*) instance { if( sharedInstance == nil ) sharedInstance = [BTBluetoothManager new]; return sharedInstance; }

Here we are assigning the delegate methods, MCNearbyServiceBrowser, MCNearbyServiceAdvertiser and MCSession to self and allocating and initializing these methods. We start by searching for the appID, advertising the appID and finally we kick of the session, which returns self.

-(id) init  { self = [super init]; if( self ) { // Capture the user-defined device name MCPeerID *myId = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]]; // Search for a 15-character max appID nearbyBrowser = [[MCNearbyServiceBrowser alloc] initWithPeer:myId serviceType:kBTAppID]; nearbyBrowser.delegate = self; [nearbyBrowser startBrowsingForPeers]; // Advertise a 15-character max appID nearbyAdvertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:myId discoveryInfo:nil serviceType:kBTAppID]; nearbyAdvertiser.delegate = self; [nearbyAdvertiser startAdvertisingPeer]; session = [[MCSession alloc] initWithPeer:myId]; session.delegate = self; } return self; }

While the below three methods are not required for the app to run successfully, leaving them in keeps Xcode from throwing an non-blocking warning. Let’s keep Xcode happy.

- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress { } - (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID { } - (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error { }

Any method that begins with (BOOL) is declaring that it will return either a YES or a NO. This method is just checking to see if a connection has been made between you and a peer and it returns that information.

// Confirming that a connection has been made between you and a peer +(BOOL) hasConnection { return (sharedInstance != nil && sharedInstance.peerName != nil); }

Now we will create a method that uses NSKeyedArchiver to pass encoded data, in our case it is our dictionary “dict,” to any session-connected peers. This method also checks for any connection errors and will print to the console if this occurs.

-(void) sendDictionaryToPeers:(NSDictionary*)dict { NSError *error = nil; NSData *encodedData = [NSKeyedArchiver archivedDataWithRootObject:dict]; [session sendData:encodedData toPeers:session.connectedPeers withMode:MCSessionSendDataReliable error:&error]; if (error) { NSLog(@"Bluetooth connection error %@", error); }

The first line of code you see below may look a little out of place. #pragma mark is used to divide your code up and make it more readable. While, #pragma mark may just look like a comment in the code, it also has the added benefit of adding visual cues to the Xcode source navigator, as seen in the image below. On the right hand side are shortcuts or jump-links to placeholders in your code – many of these set based on #pragma marks.

Pragma Marks in Xcode
Pragma Marks in Xcode

MCNearbyServiceBrowser is searching for nearby devices using the same service type (in our case this is the constant we defined as (kBTAppID @"bluetoofdemo"). Once nearby devices with this service type are located, the class gives the ability to invite those peers to a multi-peer connectivity session.

As of iOS 7, this utilizes network Wi-Fi, peer-to-peer Wi-Fi and Bluetooth. For instance, if Bluetooth was not available, connectivity would still be possible by either of the other two methods mentioned above.

In our method below, we are immediately inviting any found peers to the session.

#pragma mark - Nearby browser delegate -(void) browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info { [browser invitePeer:peerID toSession:session withContext:nil timeout:0]; } -(void) browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID { NSError *error; NSLog(@"Peer lost. Error: %@", error); }

This class method is advertising our service (remember our service-type “bluetoofdemo”?) which enables nearby peers to invite you to connect. When an invitation is recieved, MCNearbyServiceAdvertiser notifies its delegate.

In this method we are confirming that we recieved and invitation from a peer and are immediately accepting that invitation. Note: We do not need to automatically accept the invitation, typically this would be where the advertiser is given the option to accept or decline.

#pragma mark - Nearby advertiser delegate -(void) advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler { invitationHandler( YES, session ); }

Ah, now we’re getting somewhere! In the below (required) delegate method, (session:peer:didChangeState), is being called when the state of a nearby peer changes. State changes can be MCSessionStateConnected or MCSessionStateNotConnected and in our case if a “connected” state change occurs we are sending a dictionary to the peer.

#pragma mark - Session delegate -(void) session:(MCSession*)theSession peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state { if( state == MCSessionStateConnected ) { NSDictionary *handshake = [NSDictionary dictionaryWithObjectsAndKeys: @(BluetoothCommandHandshake), @"command", [[UIDevice currentDevice] name], @"peerName", nil]; [self sendDictionaryToPeers:handshake]; } else if( state == MCSessionStateNotConnected ) { } }

Now that we have a connection and this method is indicating that we have received data from a peer. In the broadest overview, this method will now connect the two (or more) peers and a short battle will ensue for who connects the fastest. Consider this a race that declares the users in 1st or 2nd place. Note: In our example we only connected between two devices, however the maximum number of connected peers is eight. The playerIndexTimestamp declares the winner and that peer is set to an index of 0.

We do all this by using a switch statement that evaluates the peerName and timeStamp and then assigning the index. We’ve also prompted an Alert View to appear which tells the user their index value.

- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:data]; NSLog(@"Dict: %@", dict); NSInteger command = [dict[@"command"] intValue]; switch( command ) { case BluetoothCommandHandshake: { _peerName = [dict objectForKey:@"peerName"]; // start negotiating player index playerIndexTimestamp = [NSDate date]; //Log player timestamp (that's you) //NSLog(@"Log of player timestamp %d", playerIndexTimestamp); NSDictionary *negotiation = @{@"command":     @(BluetoothCommandNegotiate), @"playerIndex": @0, @"timestamp":   playerIndexTimestamp}; // Logging negotiation NSLog(@"Negotiation for playerIndexTimestamp: %@", negotiation); [self sendDictionaryToPeers:negotiation]; break; } case BluetoothCommandNegotiate: { NSDate *otherTimestamp = [dict objectForKey:@"timestamp"]; // Log otherTimestamp NSLog(@"Log of other player timestamp %@", otherTimestamp); NSInteger otherPlayer = [[dict objectForKey:@"playerIndex"] intValue]; // Log other player index NSLog(@"Log of other player index %ld", (long)otherPlayer); if( [otherTimestamp compare:playerIndexTimestamp] == NSOrderedAscending ) { // other timestamp was earlier, so it wins _playerIndex = 1 - otherPlayer; NSDictionary *negotiation = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:BluetoothCommandNegotiateConfirm], @"command", [NSNumber numberWithInt:_playerIndex], @"playerIndex", nil]; NSLog(@"Another log of the negotiation %@", negotiation); [self sendDictionaryToPeers:negotiation]; dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Set Player Index" message:[NSString stringWithFormat:@"Other timestamp won, setting my index to %li", (long)_playerIndex] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }); } break; } case BluetoothCommandNegotiateConfirm: { NSInteger otherPlayer = [[dict objectForKey:@"playerIndex"] intValue]; _playerIndex = 1 - otherPlayer; dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Set Player Index" message:[NSString stringWithFormat:@"Peer confirmed my timestamp won, setting my index to %li", (long)_playerIndex] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }); break; } } [[NSNotificationCenter defaultCenter] postNotificationName:@"bluetoothDataReceived" object:dict]; } @end

So there you have it! Again, the full repo is located at https://github.com/mxstrand/iOS_Bluetooth_App.

CONCLUSION
Obviously this is a monster-length blog post. I hope you’ve enjoyed it and found value. If so, hit myself or Christian up on Twitter.

Once again, big thanks go to John Bender, John Clem, and Christian Hansen for their significant contributions to this work.

8 thoughts on “Breaking Down Bluetooth in Objective-C and iOS7

  1. awesome tutorial! Thanks for writing it.

    Unfortunately, for my needs, i find the performance of Multipeer to be sub-par.

    I’ve been using an open source iOS library called CocoaAsyncNetwork in a commercial app called tappr.tv, and I’ve found its performance to be awesome. Similar to Multipeer, it allows any device to both advertising and search for services, then connect as either server, client, or peer and send data back and forth. After optimizing my use of it, I can get 5-6 devices all talking with each other over a local-only WiFi network, with very little latency or discernible visual lag. Its suitable for professional use cases.

    To me, the promise of Multipeer is that it could do all that CocoaAsyncNetwork can do, PLUS include Bluetooth and without going thru a router. I implemented Multipeer in tappr.tv, and got it working, but there’s significant lang in the delivery of of data. movement on one device is eventually delivered to another device, but often in a stuttering fashion.

    And in fact, you can see this stuttering effect in your sample app. Is there something we’re missing, or is Multipeer simply not designed for high performance low-latency data sharing?

    1. Tx for sharing, but still too slow, the data sending via bluetooth is still much better just by using GameKit … Of course I just need a connection of two devices, but the inconvenient with GameKit, is the moment when the both devices try to connect between them. It can take 20 seconds or 2 seconds … but no latency after that

  2. Thanks for an excellent tutorial.

    I downloaded the project from https://github.com/mxstrand/iOS_Bluetooth_App and have been running it with Bluetooth only (WIFI turned off) on two iPods. I find even before the alerts appear that only one of the iPods receives a call in session:peer:didChangeState: notifying it that connection has been lost to the other peer. After this event and after pressing OK on both alerts, the peer receiving the call cannot control the bubbles on the other peer; however, the peer NOT losing the connection can still control the bubbles on both iPods. In addition, the peer losing the connection seems to have an earlier timestamp about half the time (and correspondingly a later timestamp about half the time), so I assume the loss of connection is not correlated with the timestamp order. Do you (or anyone else) know why the connection is lost for one peer? I do not see any Multipeer Connectivity framework explicit methods that are called to effect this loss of connection.

    As an aside, I do not see that the “timestamping” is used in any significant way in the code of the tutorial. Was it just put in to indicate a technique for labeling the peers when there are more than two participating in the multi peer session?

    1. Richard – thank you for your comments. I recall the buggy performance you describe and never identified or resolved root cause.

      Please note the comments from Deeje Cooley above. He published a forked version of the app using CocoaAsyncNetwork and found improved performance.

      Thanks so much for reading and commenting.

  3. Pingback: Eleanor Cauley

Leave a reply to Deeje Cooley Cancel reply