iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)
Chapter 10. Bluetooth Networking with Game Kit
One of the greatest injustices orchestrated by the iOS SDK has been the naming and accompanying reputation of Game Kit. When new developers begin to scour the depths of the iOS SDK and come across Game Kit, it is all too often dismissed as a framework specifically for games, and ignored by application developers. Game Kit contains a diverse set of features that can be beneficial in everything from productivity tools to social apps. In this chapter the focus will be on the Bluetooth networking components of Game Kit.
Currently, the only way to interact with the Bluetooth hardware of an iOS device is through Game Kit. Although some developers might yearn for the ability to interact directly with hardware, most have been grateful for the high-level wrapper that Apple has provided. Game Kit is one of the easiest frameworks to get up and running, whereas low-level networking is one of the most difficult disciplines for a new developer to become proficient with.
Game Kit was first introduced as part of iOS 3.0, adding support for Bluetooth and LAN networking as well as voice-chat services through these protocols. With iOS 4.0, Apple added Game Center functionality on top of Game Kit; for more information on Game Center, refer to Chapter 3, “Leaderboards,” and Chapter 4, “Achievements.”
Limitations of Game Kit’s Bluetooth Networking
Game Kit provides an easy-to-interact-with wrapper for communicating with other devices over Bluetooth; however, it does carry with it several important restrictions and limitations.
The most notable restriction is that the developer will have no access to the underlying functionality of Bluetooth, meaning you cannot directly request data from the hardware nor can you connect to devices that do not support Game Kit. If direct Bluetooth access is needed, Core Bluetooth should be used. From a developer standpoint you will also not be able to toggle Bluetooth on and off, although if you attempt to connect through Game Kit with Bluetooth off, the OS will prompt the user to turn it on, and provide a one-touch approach to do so.
When using the Peer Picker, which we will discuss at length in this chapter, you will not be able to connect more than one peer at a time. You can support multiple peers using your own interface, as covered in the “Advanced Features” section of this chapter. The Bluetooth standard itself also has several limitations to keep in mind during development, such as high battery drainage and limited range.
Benefits of Game Kit’s Bluetooth Networking
In the preceding section you saw some of the limitations of using Game Kit for Bluetooth networking. Although there are some serious limitations, the benefits that are gained from leveraging this technology far outweigh those limitations in most circumstances.
One of the most beneficial features of Bluetooth is that it works where there is no available Internet connection or other closed Wi-Fi networks. Bluetooth networking allows users to communicate with each other in the absence of these networks. There are hundreds of scenarios where users are not able to access outside networks such as while in flight or during road trips. Both of these environments offer a captive audience of users who might have plenty of spare time to interact with your app if they are able to do so.
The limited range of Bluetooth can also be a great benefit. By design, you can connect only to users who are physically close to you. For instance, if you have an app that transmits your business card to another user, it is much easier to find that user using Bluetooth than through an Internet connection. Bluetooth can also offer significantly lower latency than cellular or even Wi-Fi networks.
Sample App
The sample app for this chapter is a very basic copy of the iPhone’s Messages App reimplemented with Bluetooth. When it’s launched, you will be prompted to connect to another peer who has launched the app and is also searching for a peer. Upon connecting, you will be placed into a two-way chat room using Bluetooth for all communication.
The sample app has a significant amount of overhead code not related to Game Kit; most of this is to handle setting up the tableView and chat bubbles. It is important that you understand this nonrelevant code so that you can focus entirely on the functional parts of the app. As shown inFigure 10.1, the sample app uses a fairly complex tableView with variable-sized chat bubbles to display data.
Figure 10.1 A first look at the sample app.
The sample app will store all the chat information in a mutable array of dictionaries. This makes it easy to determine how many table cells need to be returned.
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [chatObjectArray count];
}
You will also need to return a customized cell for each row. Take a look at the first part of the cellForRowAtIndexPath: method. You will be working with two primary objects here. The first is a UIImageView, which will be used to display the chat bubble background, and the second is a UILabel, which will display the text contents of the message.
UIImageView *msgBackground = nil;
UILabel *msgText = nil;
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
msgBackground = [[UIImageView alloc] init];
msgBackground.backgroundColor = [UIColor clearColor];
msgBackground.tag = kMessageTag;
[cell.contentView addSubview:msgBackground];
[msgBackground release];
msgText = [[UILabel alloc] init];
msgText.backgroundColor = [UIColor clearColor];
msgText.tag = kBackgroundTag;
msgText.numberOfLines = 0;
msgText.lineBreakMode = NSLineBreakByWordWrapping;
msgText.font = [UIFont systemFontOfSize:14];
[cell.contentView addSubview:msgText];
[msgText release];
}
When creating the table cell for the first time, you will need to set up some basic defaults for each of these objects. In regard to the UIImageView, you will need to set the background color to clear as well as set a tag for referencing the object later in the method.
Creating the UILabel is only slightly more difficult; you need to set its tag and background color, and you also need to set the numberOfLines, lineBreakMode, and font. In the next snippet you will populate the pointers for the background and label with already-existing objects when the cell is being reused.
else
{
msgBackground = (UIImageView *)[cell.contentView viewWithTag:kMessageTag];
msgText = (UILabel *)[cell.contentView viewWithTag:kBackgroundTag];
}
You will need a reference to the message that will be used to populate the cell, as well as the size that displaying that message will require. In the following sections the chatObjectArray is covered in detail, but for the time being, you just need to know that a message string is being retrieved. The size variable is used to calculate the height of the table row to ensure that it accommodates the entirety of the message.
NSString *message = [[chatObjectArray objectAtIndex: indexPath.row] objectForKey:@"message"];
CGSize size = [message sizeWithFont:[UIFont systemFontOfSize:14]
constrainedToSize:CGSizeMake(180, CGFLOAT_MAX) lineBreakMode: NSLineBreakByWordWrapping];
The next step gets a bit tricky. You will need to set the backgrounds for the chat bubbles and message labels. The first thing you need to do is determine whether the message is from "myself" or from the "peer". If the message is from "myself" we want to display it on the right side of the screen using a green chat bubble; otherwise, it will be displayed on the left of the screen with the grey chat bubble.
The first line of code in each of these conditional statements sets the frame for the chat bubble. The next step is setting the bubble background with the stretchable artwork. After you have the bubble, we need to set the label inside of it and configure the resizing mask for both objects. In the second conditional block the same steps are applied but with different numbers and art to display the chat bubble on the opposite side.
UIImage *bubbleImage;
if([[[chatObjectArray objectAtIndex: indexPath.row] objectForKey:@"sender"] isEqualToString: @"myself"])
{
msgBackground.frame = CGRectMake(tableView.frame.size.width-size.width- 34.0f,1.0f, size.width+34.0f, size.height+12.0f);
bubbleImage = [[UIImage imageNamed:@"ChatBubbleGreen.png"] stretchableImageWithLeftCapWidth:15 topCapHeight:13];
msgText.frame = CGRectMake(tableView.frame.size.width- size.width-22.0f, 5.0f, size.width+5.0f, size.height);
msgBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
msgText.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
}
else
{
msgBackground.frame = CGRectMake(0.0f, 1.0f, size.width! +34.0f, size.height+12.0f);
bubbleImage = [[UIImage imageNamed:@"ChatBubbleGray.png"] stretchableImageWithLeftCapWidth:23 topCapHeight:15];
msgText.frame = CGRectMake(22.0f, 5.0f, size.width+5.0f, size.height);
msgBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
msgText.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
}
The very last thing that will need to be done is setting the bubble image and message values into the two objects for display and returning the cell as required by the cellForRowAtIndexPath: method.
msgBackground.image = bubbleImage;
msgText.text = message;
return cell;
Since each row will have a different height depending on the size of the message that needs to be displayed, you will also need to override heightForRowAtIndexPath:. The details of this method are similar to those in the previous code snippet. You will first need to get a copy of the message string and determine the height required for it to be properly displayed. Then you will return that height, in addition to 17 pixels required to fit in the chat bubble’s border.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *message = [[chatObjectArray objectAtIndex: indexPath.row] objectForKey:@"message"];
CGSize size = [message sizeWithFont:[UIFont systemFontOfSize:14]
constrainedToSize:CGSizeMake(180, CGFLOAT_MAX) lineBreakMode: NSLineBreakByWordWrapping];
return size.height + 17.0f;
}
Note
The preceding example uses variable-height table rows but doesn’t make any use of caching. Although this is acceptable for a demo on Bluetooth, it is not considered best practice when doing production development.
The Send button in the sample app also automatically enables and disables depending on whether there is text inside the text view. The following method handles that functionality:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString: (NSString *)string
{
NSUInteger textLength = [textField.text length] + [string length] - range.length;
[sendButton setEnabled:(textLength > 0)];
return YES;
}
There is some additional minor overhead that needs to be done, such as setting the text view to be the first responder and setting delegates for the table view and text fields. For a complete look at the sample app, spend some time looking over the sample code. The information that was provided in this section is more than enough to get you comfortable with the parts of the app that don’t relate to Game Kit so that you can focus on new information in the following sections.
The Peer Picker
Apple provides a built-in interface for finding other devices to connect to called the Peer Picker (Figure 10.2). The Peer Picker makes it extremely easy for your users to find peers and automates most of the functionality of connecting so that you don’t need to get your hands dirty.
Figure 10.2 The Peer Picker searching for available devices.
In the sample app, when it is launched, there is a single button in the center of the screen titled Connect to New Peer. This action will trigger the Peer Picker using the connect: method.
The connect: method starts off by allocating and initializing a new instance of GKPeerPickerController. A delegate for GKPeerPickerController also needs to be defined; in the sample project, this is done in the RootViewController. The Peer Picker can also take an argument for a connectTypeMask. The sample app specifies GKPeerPickerConnectionTypeNearby, which is used for Bluetooth connections. You can also specify GKPeerPickerConnectionTypeOnline for LAN networking or for both types together usingGKPeerPickerConnectionTypeOnline | GKPeerPickerConnectionTypeNearby.
Warning
There is a known bug in iOS 3.x that requires that you supply GKPeerPickerConnectionTypeNearby as at least one of the parameters for connectionTypeMask or you will trigger an exception. This bug has been resolved in newer versions.
- (IBAction)connect:(id)sender
{
peerPicker = [[GKPeerPickerController alloc] init];
peerPicker.delegate = self;
peerPicker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
[peerPicker show];
}
Two delegate methods need to be implemented in order for the Peer Picker to be fully functional. One is called when the Peer Picker is cancelled; you are required to set the delegate to nil here and release the picker.
- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker
{
picker.delegate = nil;
[picker release];
}
There is also a delegate callback when the user selects a peer from the Peer Picker. The peerPicketController:didConnectPeer:toSession: method provides three important arguments. The first one is the GKPeerPickerController. Here, you are required to dismiss the picker and release its memory as part of this callback.
- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session
{
NSLog(@"Peer Selected");
[picker dismiss];
[picker release];
}
The remaining two arguments are important for the app to begin the communication process between the devices. Look at the method in the sample app in the following example. The picker is still dismissed and released as in the earlier example; however, there are also a few new lines of code.
In the sample project, we have a new view controller that handles the chat functionality. The Peer Picker needs to pass the session and peerID information to this class. ICFChatViewController has two synthesized properties to hold onto this information that are set after new instances of ICFChatViewController named chatViewController are created. After that information is set, the new view is pushed onto the navigation stack, seen in Figure 10.3.
- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session
{
NSLog(@"Peer Selected");
[picker dismiss];
[picker release];
ICFChatViewController *chatViewController = [[ICFChatViewController alloc] init];
chatViewController.currentSession = session;
chatViewController.peerID = peerID;
[[self navigationController]
pushViewController:chatViewController animated:YES];
[chatViewController release];
}
Figure 10.3 Automatic peer detection using the built-in Peer Picker.
These are the bare minimum steps required when using the Peer Picker to connect to a new device. In the next section you will expand on this knowledge to begin sending data between devices.
Warning
The Peer Picker is based on the bundle ID; the app will find peers only searching from apps with the same bundle ID.
Sending Data
Connecting to a new peer is only the first step in implementing a functional peer-to-peer networking app. After connecting to a new peer, you will want to send that peer some data. Sending data using Game Kit is extremely easy. Take a look at the following code snippet:
[mySession sendDataToAllPeers:messageData withDataMode:GKSendDataReliable error:&error];
The first part of this method is a pointer to the session that was returned as part of connecting to a new peer. In the preceding example the data is being sent to all connected peers. There is an alternative method, sendData:toPeers:withDataMode:error:, if you wanted to send data to only a specific peer. The next part of the code snippet is an argument called messageData. This needs to be of the NSData type. You also have to specify a data mode, which is covered later in this section. (The example uses GKSendDataReliable.) Finally, there is a reference to an error to catch anything that goes wrong.
Data Modes
When data is sent using Game Kit, there are two possible valuables for the dataMode argument: GKSendDataReliable and GKSendDataUnreliable. It is important to understand the distinction between these methods:
GKSendDataReliable tells Game Kit that this data is required to be received by the peer; if it is not received, it will be resent until it is received. In addition to continually retrying to send the data, GKSendDataReliable ensures that data packets are received in the same order in which they are sent. This method is used in the sample app since we want text messages to never get lost in the network and we want them to appear in the order in which they are sent.
GKSendDataUnreliable, on the other hand, means that the data is not resent if it is not received; each packet is sent only once. This means that the networking is faster since the originating peer does not have to wait for confirmation that the packet has arrived. In addition, packets might not arrive in the order in which they were sent. GKSendDataUnreliable is very useful for fast networking and keeping two clients in sync when the data isn’t entirely crucial to the app. For example, if you have a racing game, you will want to send the position of your car to the other player. However, if a packet doesn’t reach its destination, it is better to skip it and sync up with the next packet; otherwise, the data you receive will be old and out-of-date. Each client would slowly fall further out of sync with the other.
There is a general rule of thumb for helping you decide which data mode you should use for your app. Use GKSendDataReliable when the data and order are more important than the time it takes to arrive. Use GKSendDataUnreliable when the speed at which the data is received is more important than the order and the data itself.
Sending Data in the Sample App
Take a look at how sending data is implemented in the sample app; everything is handled directly in the sendMessage: method. Because the app will need to send more data than just the message string, a new dictionary is created to also store information about who the message came from. For the purposes of the sample app, there is a mutable array that will hold onto all the incoming and outgoing data called chatObjectArray. Because we are sending a message, a local copy is inserted into that array.
Earlier, you learned that you can send only NSData. However, you do not want to send the dictionary since the sender key is set to "myself"; when the other peer receives a message, the sender should not be set to "myself". A new NSData object is created with just the message string, and a dictionary object can be created after the message has been received.
Now that the message has been translated into an NSData object and a new NSError has been created, the session that was created when the app connected to a peer can call sendDataToAllPeers:withDataMode:error: with the newly created information. You will want to make sure that no errors were returned; if an error is returned, it is logged to the console in the sample app.
- (IBAction)sendMessage:(id)sender
{
NSDictionary *messageDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:[inputTextField text], @"message", @"myself", @"sender", nil];
[chatObjectArray addObject: messageDictionary];
[messageDictionary release];
NSData *messageData = [inputTextField.text dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
[self.currentSession sendDataToAllPeers:messageData withDataMode:GKSendDataReliable error:&error];
if(error != nil)
{
NSLog(@"An error occurred: %@", [error localizedDescription]);
}
[inputTextField setText: @""];
[sendButton setEnabled: NO];
[chatTableView reloadData];
//scroll to the last row
NSIndexPath* indexPathForLastRow = [NSIndexPath indexPathForRow:[chatObjectArray count]-1 inSection:0];
[chatTableView scrollToRowAtIndexPath:indexPathForLastRow atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
The sample app has some additional cleanup that needs to be performed whenever a message is sent, such as clearing the text field and setting the Send button back to disabled. In addition, the chatTableView is reloaded and scrolled to the bottom to reflect the new outgoing message. In the next section you will learn how to receive the data that was just sent.
Receiving Data
Sending data is pointless if the receiving peer is not set up to handle that data. The first thing that needs to be done when preparing your app to receive data is letting the GKSession know which class will handle the incoming messages by setting the dataReceiveHandler. An example is shown in the following code snippet:
[self.currentSession setDataReceiveHandler:self withContext:nil];
A data handler needs to be set for each session and is session-specific. You can define a context as part of the setDataReceiveHandler: and this context will be passed along as part of any incoming data calls.
Every time your session receives data, your data handler will call receiveData:fromPeer:inSession:context. The following example prints the received NSData to the log:
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context
{
NSLog(@"Data Received: %@", data);
}
Receiving Data in the Sample App
The previous examples are really the simplest form of receiving data. Most of the time, you need to convert the NSData back into whatever format you want to work with, such as an NSString or a UIImage. In addition, you will probably want to do something with that newly received data beyond logging it. Take a look at the receiveData:fromPeer:inSession:context: method in the sample app, shown in the following code snippet. Because the sample app is working with a string, the first thing that needs to be done is retrieve the value back out of theNSData. After you have the NSString, you need to store it into the expected data format of an NSDictionary. This is the same approach taken in the sending data methods, except that the "sender" key "peer" is set instead of "myself". This allows it to be displayed correctly in the table view. After the new dictionary is inserted into the chatObjectArray, the chatTableView is refreshed and scrolled to the bottom to make the newest message visible.
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context
{
NSString *messageString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *messageDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:messageString, @"message", @"peer", @"sender", nil];
[chatObjectArray addObject: messageDictionary];
[messageString release];
[messageDictionary release];
[chatTableView reloadData];
//scroll to the last row
NSIndexPath* indexPathForLastRow = [NSIndexPath indexPathForRow:[chatObjectArray count]-1 inSection:0];
[chatTableView scrollToRowAtIndexPath:indexPathForLastRow atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
State Changes
When working with remote peers, it is important to know the state of the remote device because networks and connectivity can unexpectedly drop. You will want to inform your user and perform appropriate error-recovery behavior in these events. To monitor state changes, you must first declare a class as the delegate, make sure that your class conforms to GKSessionDelegate, and then set the delegate for the session in a manner similar to the following example. Note that this is different from the dataReceiveHandler, which can be set to a different class.
[self.currentSession setDelegate: self];
Your newly set delegate will now receive calls to session:peer:didChangeState:. This code snippet is from the sample app, in which a disconnected peer is caught and we present the user with an alert informing them of such. See Table 10.1 for a complete list of possible states.
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
if(state == GKPeerStateDisconnected)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert!" message:@"The peer has disconnected" delegate:nil cancelButtonTitle: @"Dismiss" otherButtonTitles: nil];
[alert show];
[alert release];
[self.currentSession disconnectFromAllPeers];
[[self navigationController] popViewControllerAnimated: YES];
}
}
Table 10.1 Possible State Changes and Their Definitions
Advanced Features
In previous sections, you learned how to create a new basic connection using the Peer Picker and sending and receiving data from that peer. Game Kit’s Bluetooth networking has several advanced features, some of which are covered in this section.
Peer Display Name
It is often useful to display the name of the peer you are connected to. Referring to Figure 10.1, the navigation bar contains a title that references the connected user; this is done using the following code snippet:
NSString *peerDisplayName = [self.currentSession displayNameForPeer: self.peerID];
[[self navigationItem] setTitle: [NSString stringWithFormat:@"Chat with %@", peerDisplayName]];
Connecting Without the Peer Picker
There are times when you might want to connect using Game Kit but without displaying the Peer Picker. Although this is a slightly more difficult approach, it is still entirely possible. First, you need to create a new session, as shown in the following example:
currentSession = [[GKSession alloc] initWithSessionID:nil displayName:nil sessionMode:GKSessionModePeer];
currentSession.delegate = self;
currentSession.available = YES;
currentSession.disconnectTimeout = 15;
[currentSession setDataReceiveHandler:self withContext:nil];
There are some key differences in the preceding example from implementing a connection using the Peer Picker. First, you need to create the GKSession instead of the Peer Picker returning one during the connection callback. The sessionID argument can be used to define matching peers to connect to. If the session IDs do not match, the devices will not see each other. In the following section you will learn more about the sessionMode argument. You will also need to set the available property to YES, which is an important step for other devices to see the device as ready for a connection. A disconnectTimeout is also set for 15 seconds.
When you set a device to available, it will inform any other devices looking for peers that it has become available for connections. Because there is a state change from unavailable to available, the GKSession delegate will receive a callback to session:peer:didChangeState:. In the following example, when the app detects that a new device has become available, it immediately tries to connect. In real-world implementations, you might want to present your user a list of IDs before they pick one to connect to.
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
if(state == GKPeerStateAvailable)
{
[session connectToPeer:peerID withTimeout:15];
}
}
When a device attempts to connect to your device, you receive a call to didReceiveConnectionRequestFromPeer:, which must be handled similarly to the next code snippet:
- (void)session:(GKSession*) session didReceiveConnectionRequestFromPeer:(NSString*) peerID
{
[session acceptConnectionFromPeer:peerID error:nil];
}
After you have called acceptConnectionFromPeer:, your delegate will receive another call to session:peerID:didChangeState: with a GKPeerStateConnected state. From here, you can continue with your app as if you had connected using the Peer Picker.
Note
Whereas the Peer Picker has a two-person limit on connections, connecting without the Peer Picker allows up to four total devices.
Session Modes
In the preceding section, you learned how to connect to a peer without using the Peer Picker. Part of creating a new GKSession was specifying a sessionMode; in the previous example, the mode supplied was GKSessionModePeer. There are three types of sessionModes that can be supplied.
GKSessionModePeer acts like the Peer Picker. It looks for a peer at the same time as it allows other peers to find it. Either side can initiate the connection when working with a GKSessionModePeer.
GKSessionModeServer acts differently than GKSessionModePeer. In this mode the GKSession will not look for other peers but will make itself available to be connected to by peers that are searching.
GKSessionModeClient is the reverse of GKSessionModeServer. It searches for peers to connect to but does not allow other peers to search for it.
Summary
In this chapter, you learned how to fully implement a peer-to-peer Bluetooth network using two iOS devices. The limitations and capabilities of Game Kit’s Bluetooth networking were discussed, as well as some of the benefits that can be leveraged by including Bluetooth networking. The sample app for this chapter walked you through adding networking to a simple two-way chat app.
You worked with sending data and the different options available to you when transmitting, as well as how to receive and process data. In addition, you learned how to handle disconnects and other important state changes from the connected peer. There was also a short section on some of the more advanced features of Game Kit’s Bluetooth networking. You should now have all the knowledge and confidence to be able to successfully add Bluetooth networking into your iOS app in minimal time.
Exercises
1. Add the capability to send and receive images into the sample app. Remember that you will need to add a way to identify the message as either an image or a string.
2. Implement a custom Peer Picker to find and establish a connection with another device.