Bleeding Edge Press Developing an iOS 7 Edge (2013)

5. AirDrop and Multipeer Connectivity

IN THIS CHAPTER

·        Using AirDrop to send files between iOS devices

·        How to adapt our own app to take advantage of AirDrop

·        Basics of Multipeer Connectivity

·        Advertising and dicovering peers

·        Sending data and resources among peers

·        Authentication and encryption

AirDrop

AirDrop is a technology first introduced by Apple in OS X 10.7 Lion. It enables Macs to easily share files. Sharing files between AirDrop capable machines is as easy as activating AirDrop in Finder and dragging the files onto the icon for the desired machine after the nearby computers are shown. AirDrop sharing magically works even if the machines don't have an Internet connection. It even works if the machines are not connected to the same WiFi access point, or to any access point for that matter.

AirDrop works by establishing an adhoc Wi-Fi connection between the participating Macs, thus removing the need for the Internet and an infrastructure wireless network.

Now, with iOS 7, Apple is bringing this technology to the world of iPhones and iPads. iDevices can share files and documents among each other as long as Wi-Fi and Bluetooth are both turned on (as of this writing, AirDrop on iOS and Mac is not compatible).

How to Use AirDrop

The BepBop app can send and receive PDF files over AirDrop. When you tap to open one of the PDF files listed in the table view in the "Direct Wireless Connectivity" section of the app, you will see an 'action' button at the top right corner of the screen. Tapping that button allows you to send the PDF document over various methods including AirDrop:

Just tapping on their AirDrop icon sends the file to them and the receiver sees the following confirmation:



If there are multiple apps installed that support the same file, they also get to choose which app should open the file you just sent. In this case since we are sending a very common file format, PDF, there are many apps that can handle it, including our demo app, BepBop.



So using AirDrop to share files could not be any easier from a user's perspective. But how do we add AirDrop support to our own apps? In order to show you how this is achieved we build AirDrop support in our demo app, BepBop, and will now walk you through the process.

Adding AirDrop Support to your App

To enable your own app to have the option to share files over AirDrop, you need use the system provided class UIActivityViewController. In the BepBop app we are initilizing an instance of UIActivityViewController in the BEPSimpleWebViewController class:

 0001:- (void)presentActivities:(id)sender
0002:{
0003:    UIActivityViewController * activities = [[UIActivityViewController alloc] initWithActivityItems:@[_url]
0004:    applicationActivities:nil];
0005:   
0006:    [self presentViewController:activities
0007:                       animated:YES
0008:                     completion:nil];
0009:}

Where _url is the file URL of the opened PDF file. After initizalization, the activities controller is presented modally. If there are any other devices with AirDrop turned on nearby, the activity controller will automatically show them and handle the file transfer for you.

That's it! No, really, you don't need to do more to send files over AirDrop from your app! 

Receiving Files over AirDrop

Sending files over AirDrop was unbelievably easy, but accepting files over AirDrop and correctly handling them is a little bit more involved but nothing too complicated. There are two steps: First you need to declare your app as an handler to the files types you're capable of handling. And second once the system notifies your app of the reception of such a file you need to take it and copy it to a local directory from its temporary AirDrop location. Let's look at how we have done it for the BepBop app:

Declaring Your App as a Handler for a File Type

Apple uses the universal type identifiers (UTIs) to decide which resources can be handled by which apps. You can go readup on the details of the UTIs but for our purposes now UTIs are strings like public.image or com.adobe.pdf,which identify different formats of data and the system decides on a particular UTI for an item by looking at the MIME-type or the file extension.

If we want our app to be recognized as a handler for a certain file type we have to declare in as such by adding the corresponding UTI in our Info.plist. In Xcode 5 this can be achieved by going to the Info tab under our build target and adding our desired UTI under "Document Types". Here is an example of how we declared the BepBop.app to a handler for PDF files:

One undocumented fact to keep in mind while deciding on which UTIs to handle with your app is that Apple does not treat all UTIs equally. Some UTIs such as public.image, public.jpeg and public.png are private and can only be handled by the default Photos.app on your iOS devices. This was probably a decision on Apple's part to protect users from being overwhelmed by all the photo apps which would complete to open their photos every time they received a photo over AirDrop.

Receiving Files Through Airdrop in Your App Delegate

You need to implement the method application:openURL:sourceApplication:annotation: in your application delegate to be able to receive files through AirDrop. When the system routes an AirDrop file to your app, this method will get called and the url will point to a file in your app's /Documents/Inbox directory. This directory is a special directory where your app has read permission but is unable to write to. If you need to modify the file you must move it to another directory first. Another point to pay attention to is, that the files in this directory can be encrypted using data protection. In the normal case the files inside your apps documents directory are freely available but in some cases, by the time this method gets called the user could have already locked the device. Therefore it is a good idea to check if the file is readable first by using the protectedDataAvailable property of the application object. Apple recommends to save the AirDrop URL for later and returning YES from this method even if the file could not be accessed due to data protection.

This method is a general purpose delegate method which gets called whenever the system tells your app to open a URL. Before iOS 7, apps used this functionality to pass data between apps by declaring their custom URL protocol and their own little API, like twitter://post?message=hello%20world&in_reply_to_status_id=12345. Starting with iOS 7 this method is also used by the system to tell your app that there is an incoming AirDrop file. To be able to distinguish between the opening a normal URL and receiving an AirDrop file cases, we can make use of several pieces of information:

·        In the case of AirDrop sourceApplication is nil

·        In the case of AirDrop url always contains /Documents/Inbox

·        AirDrop url protocol will always be file://

Here is how we implemented the application:openURL:sourceApplication:annotation: method in the BepBop.app:

 0001:- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
0002:{
0003:    if (sourceApplication == nil &&
0004:        [[url absoluteString] rangeOfString:@"Documents/Inbox"].location != NSNotFound) // Incoming AirDrop
0005:    {
0006:        NSLog(@"%@ sent from %@ with annotation %@", url, sourceApplication, [annotation description]);
0007:        if (application.protectedDataAvailable) {
0008:            [[BEPAirDropHandler sharedInstance] moveToLocalDirectoryAirDropURL:url];
0009:        }
0010:        else {
0011:            [[BEPAirDropHandler sharedInstance] saveAirDropURL:url];
0012:        }
0013:        return YES;
0014:    }
0015:
0016:    return NO;
0017:}

As you can see from the code above, it is quite straightforward to handle an incoming AirDrop file. If we have access to protected data we call the moveToLocalDirectoryAirDRopURL: method, which moves the file given to us by AirDrop to a local directory where we can modify the file as we see fit:

 0001:- (BOOL)moveToLocalDirectoryAirDropURL:(NSURL *)url
0002:{
0003:    NSFileManager * fm = [NSFileManager defaultManager];
0004:   
0005:    NSURL * localUrl = [[self airDropDocumentsDirectory] URLByAppendingPathComponent:[url lastPathComponent]];
0006:    NSError * error;
0007:    BOOL ret = [fm moveItemAtURL:url
0008:                           toURL:localUrl
0009:                           error:&error];
0010:   
0011:    if (!ret) {
0012:        NSLog(@"could not move file from %@ to %@: %@", url, localUrl, error);
0013:    }
0014:   
0015:    return ret;
0016:}
0017:

 On the other hand if protected data is not available for the time being because the device is locked with a passcode, we fallback to saving the incoming file URL in user defaults for later processing:

 0001:- (void)saveAirDropURL:(NSURL *)url
0002:{
0003:    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
0004:    NSArray * currentStoredURLs = [defaults arrayForKey:BEPDefaultsStoredAirDropURLsKey];
0005:   
0006:    if (currentStoredURLs == nil) {
0007:        currentStoredURLs = @[];
0008:    }
0009:   
0010:    NSArray * updatedStoredURLs = [currentStoredURLs arrayByAddingObject:url];
0011:   
0012:    [defaults setObject:updatedStoredURLs forKey:BEPDefaultsStoredAirDropURLsKey];
0013:   
0014:    [defaults synchronize];
0015:}

The best way to process these saved URLs later is to add an observer to the UIApplicationProtectedDataDidBecomeAvailable notification and in the handler call the moveToLocalDirectoryAirDropURL: for each element of the saved URLs array.

The AirDrop feature is quite powerful and easy to use but does not provide an API to integrate more tightly in our apps. Luckily, however, Apple also introduced a new framework in iOS7, which enables us to build on top of the same technologies as AirDrop. Let's see how we can use it in the next section.

Multipeer Connectivity

In iOS 7, Apple added a completely new framework called Multipeer Connectivity. This framework provides an easy interface to the powerful wireless technology underlying AirDrop and enables peer-to-peer wireless data transfer over bluetooth and adhoc Wi-Fi among groups of peers. iOS developers could build completely new and amazing apps using this new capabilities.

Technical Details

The Multipeer Connectivity (MC) framework enables both the discovery of services provided by nearby iOS devices and transfer of data between peers over infrastructure Wi-Fi networks, adhoc Wi-Fi and Bluetooth personal area networks. The low-level and wireless technology depended complexities are beautifully abstracted away so that the we, as app developers, can concentrate on developing new apps and features using the new services and functionality provided by the MC framework.

In the discovery phase, the MC-framework uses Apple's Bonjour technology to advertise and discover services and subsequently configure connections. 

During data transfer the MC-framework intelligently uses the available wireless networks to make sure that the data is received by all peers in the session.

In the figure above, Peers A, B and C are in the same session. A has both Wi-Fi and Bluetooth interfaces available where as B and C only have Bluetooth and Wi-Fi respectively. In such a configuration when A sends data to the session, both B and C receive the data through their available interfaces. However since B cannot communicate with C directly due to the lack of a common interface, the MC-framework ensures that the data sent by B are transparently relayed via A to C such that every peer in the session can communicate with the other peers independent of the underlying network technology.

Basic Usage

Before you can start exchanging data between the Peers you first need to setup the session. This involves advertising your service and browsing for other peers. You see it action in our demo app BepBop. We will walk through the details of this initial phase of the setup in the next couple of sections and see how it all works together to give the user a seamless and easy connection experience.

How to Advertise Your Service – MCAdvertiserAssistant

Before a session can be joined it must be initialized and advertised so that other peers can find and join it. The most straight-forward way to do this is by using an advertising assistant. An advertising assistant sits on top of the MCNearbyServiceAdvertiser, which advertises our service over the network, and takes care of various tasks such as showing invitation confirmations and such. We do this by initializing an instance of the class MCPeerID, which will identify our device for the purposes of MC-framework and we are responsible for keeping it unique in our apps:

 0001:    NSString* deviceName = [[UIDevice currentDevice] name];
0002:    self.myPeerID = [[MCPeerID alloc] initWithDisplayName:deviceName];

Then we can initialize an empty session with our peer ID and assign a delegate to it. In the example code we will be implementing the MCSessionDelegate protocol in our view controller and can thus assign self as delegate:

 0001:    self.session = [[MCSession alloc] initWithPeer:_myPeerID];
0002:    _session.delegate = self;

Now we're ready to setup our advertiser assistant by initialzing an instance of MCAdvertiserAssistant with our service type and session. Since the advertiser assistant also needs a delegate to inform us of received invitations, we will implement MCAdvertiserAssistantDelegate in our view controller and assign self also as the delegate to our advertiser assistant and finally start the assistant:

 0001:    self.assistant = [[MCAdvertiserAssistant alloc] initWithServiceType:BEPMultipeerConnectivityServiceType
0002:                                                          discoveryInfo:nil session:_session];
0003:    _assistant.delegate = self;
0004:    [_assistant start];
0005:

Now that our service is running and being advertised over the network, it is a good time discuss the details of the two delegate protocols we need to implement. MCSessionDelegate protocol is used to inform us of changes in session membership and data reception. We will go into the details of data reception in the following sections but for the purposes of advertising a service the most important method in the session delegate is session:peer:didChangeState:which tells us if peers have succesfully joined our session or not:

 0001:- (void) session:(MCSession*)session peer:(MCPeerID*)peerID didChangeState:(MCSessionState)state
0002:{
0003:    dispatch_async(dispatch_get_main_queue(), ^{
0004:                       [self updateUIState];
0005:                   });
0006:}
0007:

The other protocol we just claimed to implement (by providing empty implementations), MCAdvertiserAssistantDelegate, allow us to intercept just before an invitation confirmation will be shown and just after it was dismissed. The actual process of receiving invitations, showing confirmations for them and sending out acceptances or rejections appropriately is taken care of by the advertiser assistant itself (in the following sections we will cover the details of how to programmatically handle invitations with the MCNearbyServiceAdvertiser and its corresponding delegate). 

The advertiser assistant shows the following confirmation when it receives an invitation to connect. You can either accept to let the peer join the advertised session or reject. 

How to Browse for Services – MCBrowserViewController

Now that we covered how to advertise our service, let's look at how other peers can discover it and join its session. MCBrowserViewController is the easy to use standard way of browsing for specific service types, which does most of the heavy lifting associated with finding peers and sending out invitations. First we need to initialize our peer ID and an empty session, just like we did for the MCAdvertiserAssistant, before we can initialize a MCBrowserViewController with the service type we want to browse for. Once we have the browser, we assign a delegate implementing MCBrowserViewControllerDelegate and present it on our view controller:

 0001:- (IBAction) browseButtonTapped:(UIButton*)sender
0002:{
0003:    NSAssert(_session != nil, @"trying to start browser but our session is nil");
0004:    self.browser = [[MCBrowserViewController alloc] initWithServiceType:BEPMultipeerConnectivityServiceType
0005:                                                                session:_session];
0006:    _browser.delegate = self;
0007:    [self presentViewController:_browser
0008:                       animated:YES
0009:                     completion:nil];
0010:}

The browser shows a list of nearby peers advertising the requested service type. Tapping one of the peers sends them an invitation that they can either accept or decline. The invitation could either be handled programmatically by the other peer (discussed in the following sections) or shown by an advertiser assistant (see above).

Sending Data Between Peers

Once we have other peers in a session we can start sending and receiving data. The MC-framework abstracts away all network-level functionalities and provides an easy-to-use API in the form of MCSession and MCSessionDelegate, allowing sending and receiving of three types of objects: 

1.    Messages as NSData instances

2.    Resources as NSURL instances

3.    Data streams  as NSStream instances

Let's quickly introduce the methods involved in each case.

Sending Messages

Messages are sent by a MCSession instance's sendData:toPeers:withMode:error: method and handled by the delegate's session:didReceiveData:fromPeer: method at the other end. Even though there is no explicit limit on the size of data that can be sent using these methods, the best practice is to use these methods only for small messages because they don't provide any way to track the progress of the transfer. Let's look at how we send an example dictionary when the user taps the "Say Hello" button in the demo app:

 0001:    NSDictionary * message = @{@"message": @"Hello World!"};
0002:   
0003:    NSString * errorString = nil;
0004:    NSData * data = [NSPropertyListSerialization dataFromPropertyList:message
0005:                                                               format:NSPropertyListXMLFormat_v1_0
0006:                                                     errorDescription:&errorString];
0007:    if (errorString) {
0008:        NSLog(@"could not serialize\n%@\n%@", message, errorString);
0009:    }
0010:    else {
0011:        NSError * error;
0012:        if (![_session sendData:data
0013:                   toPeers:[_session connectedPeers]
0014:                  withMode:MCSessionSendDataReliable
0015:                          error:&error])
0016:        {
0017:            NSLog(@"failed sending data");
0018:        }
0019:    }

The delegate parses the received data as follows:

 0001:    NSPropertyListFormat fmt;
0002:    NSError * error;
0003:    NSDictionary * message = [NSPropertyListSerialization propertyListWithData:data
0004:                                              options:NSPropertyListImmutable
0005:                                               format:&fmt
0006:                                                error:&error];
0007:    if (message == nil) {
0008:        NSLog(@"could not parse received data: %@\n%@", error, data);
0009:        return;
0010:    }

Because we sent with the reliable mode using the MCSessionSendDataReliable parameter, the MC-framework will automatically retry in the case of dropped packages and guarantee an in order delivery using send and receive buffers. This mode functions similar to TCP protocol and while making it straight-forward to reliable transfer data is not suitable for multi-media data where latency is more critical than reliability. Low-latency, non-reliable mode which pushes data immediately can be selected by using the MCSessionSendDataUnreliable parameter.

Sending Resources

If you need to transfer larger files, your best solution is to use the sendResourceAtURL:withName:toPeer:withCompletionHandler: method in MCSession. When given a file URL this method will reliably send that file to the specified peer and call its completion handler once the transfer is complete. In the following code snipplet you can see how the BepBop app uses the resource sending functionality of MCSession to transmit a photo to another peer:

 0001:    NSURL * tempURL = // write image data to a temporary location
0002:   
0003:    // the completion handler which shows either a success or error message
0004:    id completionHandler = ^(NSError* error){
0005:        if (error) {
0006:            // Show failure alert
0007:        }
0008:        else {
0009:            // Show success alert
0010:        }
0011:    };
0012:    //send the temp file to all connected peers in session.
0013:    for (MCPeerID * peer in _session.connectedPeers) {
0014:        [_session sendResourceAtURL:tempURL
0015:                           withName:[tempURL lastPathComponent]
0016:                             toPeer:peer
0017:              withCompletionHandler:completionHandler];
0018:
0019:    }

We have removed the tedious part of the code to emphasize the important parts. Please note that unlike sending messages, resources cannot be sent to multiple peers at once. That's why we're looping through all connected peers in session. The completion handler is a block gets passed an error parameter once sending is completed. If the error parameter is nil the transmission was completed successfully, the error object will otherwise contain information about what went wrong.

On the other end, the delegate method session:didStartReceivingResourceWithName:fromPeer:withProgress: will be called when the transfer starts. This is a good place to initialize the UI which shows the status of the file transfer using the progress object. Once the transfer is complete the delegate method session:didFinishReceivingResourceWithName:fromPeer:atURL:withError: is called. If there are no errors the received file is located at a temporary location and the receiver is responsible to copy the received file to the desired location.

Sending Streams

If you need a low level stream to another peer to pipe data as it becomes available, you should look into the startStreamWithName:toPeer:error: of MCSession and the corresponding session:didReceiveStream:withName:fromPeer:method of the MCSessionDelegate. Once the streams have been established they can be used as any other output or input stream by the sender and respectively the receiver. We will not go into further detail here since most apps will not need such low-level network streams functionality. Detailed info can be found in Apple documentation.

Advanced Cases

We have already seen how to advertise and browse for services using the provided convenience classes MCAdvertiserAssistant and MCBrowserViewController, which also take care of the invitation process for us. The MC-framework also offers the possibility to programmatically control the advertising and browsing processes to obtain more flexibility and offers the possibility to build a custom browsing UI. Moreover, authentification of peers and encryption of transmitted data is also supported. Let's see how these work in the next three sections.


Programmatically Advertising and Handling Invitations

If you need more control on the advertising and invitation processes, the MCNearbyServiceAdvertiser and the corresponding delegate protocol MCNearbyServiceAdvertiserDelegate provide deeper programmatic control. The biggest difference to the advertiser assistant is that we are responsible for adding and removing peers to the session, and handling invitations but we have more options when advertising the service. An instance of MCNearbyServiceAdvertiser is initialized using initWithPeer:discoveryInfo:serviceType:, where discovery info is a dictionary of key-value pairs, which is attached to our advertisement as a Bonjour TXT-record. This provides an extra channel where can put some information for the peers browsing for our service type:

The corresponding MCNearbyServiceAdvertiserDelegate protocol defines the advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler: method which gives us complete control on how to handle an incoming invitation. Whereas the MCAdvertiserAssistant shows a UIAlert to let the user accept or reject the invitation, using this delegate method we have the possibility to come up with ways to allow automatic acceptance or rejection of peers based on our app's criteria.


Programmatically Browsing for Peers

Similarly to MCNearbyServiceAdvertiser, MCNearbyServiceBrowser provides complete control on the browsing process and gives a foundation to build a custom browsing UI through it delegate protocol MCNearbyServiceBrowserDelegate and its methods browser:foundPeer:withDiscoveryInfo: and browser:lostPeer:

The MCNearbyServiceBrowser method invitePeer:toSession:withContext:timeout: is used to invite other peers into a session allows passing auxiliary data with the context parameter to advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler: method of the advertiser delegate. The outline of such an automated algorithm could look like the this:

·        Browser peer invites the other peer with a key as context data

 0001:    [browser invitePeer:advertiserPeer
0002:              toSession:_mySession
0003:            withContext:@{@"key": myBase64EncodedKey}
0004:                timeout:60];
0005:

·         Advertiser delegate on the other peer checks if this key is allowed

 0001:- (void)            advertiser:(MCNearbyServiceAdvertiser *)advertiser
0002:  didReceiveInvitationFromPeer:(MCPeerID *)peerID
0003:                   withContext:(NSData *)context
0004:             invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler
0005:{
0006:    NSString * base64EncodedKey = context[@"key"];
0007:   
0008:    BOOL keyValid;
0009:   
0010:    // Check if the provided key is valid
0011:   
0012:    if (keyValid) {
0013:        invitationHandler(YES, _mySession);
0014:    }
0015:    else {
0016:        invitationHandler(NO, nil);
0017:    }
0018:}

Authentication and Encryption

In addition to the powerful discovery and data transfer features, the MC-framework also supports industry standard authentication and encryption methods. A secure session can be created like this:

 0001:MCSession *session = [[MCSession alloc]
0002:                      initWithPeer:myPeerID
0003:                  securityIdentity:identity
0004:              encryptionPreference:preference];

where identity is an array containing a SecIdentityRef object and (optionally) additional certificates needed to verify that identity and preference is one of MCEncryptionNone, MCEncryptionOptional or MCEncryptionRequired. An unencrypted session can only interact with unencrypted peers and a session with mandatory encryption can only interact with encrypted peer whereas a session with optional encryption can accept both encrypted and unencrypted peers.

When a peer tries to establish connection to a secured session, the session delegate method session:didReceiveCertificate:fromPeer:certificateHandler: will be called. It is the session delegates responsibility to validate the provided chain of certificates and either accept or reject the connection.

Summary

In this chapter we have introduced the new AirDrop feature in iOS7 and covered how you can add AirDrop support to your apps. Then we introduced the Multipeer Connectivity framework, on top which the AirDrop feature is built. Using the MC-framework it is possible to easily create apps, which communicate with their nearby peers to enable previously impossible new features. We first walked through how advertise and discover services using the high-level classes in the framework. Then showed how you can send and receive data once the connection has been established between the peers. And finally moved on to the more advanced features of the framework such as custom advertisement and invitation methods, how to provide a custom browsing UI and finally how to setup secure sessions.