Eyes, JAPAN Blog > Introduction in Multipeer Connectivity Framework. Basic concepts and examples.

Introduction in Multipeer Connectivity Framework. Basic concepts and examples.

Mola BogdanGeorgii

この記事は1年以上前に書かれたもので、内容が古い可能性がありますのでご注意ください。

Any mobile developer eventually encounter necessity to implement some network interaction between devices. IOS development is not exception. It is fair to note that, dealing with networking is not trivial task beginners, especially if there is no time for understanding all “under the hood” specifics. Fortunately, iOS developers offer special framework to cover basic connectivity features.  This article aimed to provide understanding principles and description of basic terms, which are very important because they reflects some metaphors. In the second half of article you can find code written in Swift that is kind of “starter kit” for MultipeerConnectivity users. All provided code pieces are tested and we will use it in real project.

According to the Apple’s official documentation:

The Multipeer Connectivity framework supports the discovery of services provided by nearby devices and supports communicating with those services through message-based data, streaming data, and resources (such as files). In iOS, the framework uses infrastructure Wi-Fi networks, peer-to-peer Wi-Fi, and Bluetooth personal area networks for the underlying transport. In macOS and tvOS, it uses infrastructure Wi-Fi, peer-to-peer Wi-Fi, and Ethernet.” (This article will be focused on iOS side of framework.)

In case of iOS, framework allow to connect devices in local area. Communication suppose presence of ability to create connection, definition of participant, claim participation and finally perform data exchange. In Multipeer Connectivity, basic concepts represented as browsing, advertising, session and peer. Let’s consider them more detailed.

Peer. Basically, object of MCPeerID class is first thing that should be prepared because it is represents a device. Moreover, developer uses it to initialize instances of other classes (described below). It has important property – displayName which will be available for other devices.

Session. Object of class MCSession that handles communication (for example sending file or start stream) between connected devices. Session established when one peer invited the second. Session established between two devices.

Browser. Represented as MCNearbyServiceBrowser class. Methods of this class allow to initiate searching and invitation of other devices. Other devices should advertise themselves.

Advertiser. Class MCNearbyServiceAdvertiser and MCAdvertiserAssistant allow device to discover itself and indicates that others can invite it to a session.

The following scheme show set of usual cases for framework. Of course, there is can be more than one session.

Whole workflow consist of two phases: Discovery and Session. As you can see, Advertising and browsing belongs to discovery. At this period, developer can use discoveryInfo to provide any context data before connection established.

There is minimal requirement for session: at least one device have to enable advertising and other have to enable browsing. Both devices can keep both modes active as well. Once invitation accepted and session established device could receive/send data, resources and streams.

Device can accept invitation automatically or after user permission. Obviously, it depends on developers goals. There is even standard UI element available. MCBrowserViewController supports basic features of indication and invitation mechanism.

All events like Peer discovery, connection, disconnection, and data transferring handled through delegate callbacks. If the app moves into background framework stops advertising and browsing and it is restored when app returning to the foreground. Developer should organize reconnection to session.

Application skeleton.

After description of main concepts, let us have a look at basis for application. Assuming that separate file handles connectivity (suppose it is called ConnectionManager), that file may have the following appearance. First, we import framework into project.


import  MultipeerConnectivity

Next, it is necessary to implement four instances which were described above:


var session: MCSession! 
var peer: MCPeerID! 
var browser: MCNearbyServiceBrowser! 
var advertiser: MCNearbyServiceAdvertiser!

We will need to store somewhere list of discovered peers. As you will see next, program needs invitation handler.


var foundPeers = [MCPeerID]()

In order to employ all functionality, we need to conform some protocols. Every protocol name reflect what it do.


class ConnectionManager: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate

Now we need to provide way for self-identification of device. Created before peer variable should be initialized with name as parameter. It worth to note that example using device name, but it is not the best practice. In real project, it is wise to set name defined by user or by some rule.


    override init() {
       super.init() 
       peer = MCPeerID(displayName: UIDevice.currentDevice().name)
}

As peer exist, we use instance to initialize session object and set class as delegate of session.


override init() {
    	... 
    	session = MCSession(peer: peer)
   	 session.delegate = self
     }

It is turn of browser:


override init() {
    ... 
    browser = MCNearbyServiceBrowser(peer: peer, serviceType: "appcoda-mpc")
    browser.delegate = self
}

Browser takes peer as parameter. Then it takes serviceType. It is important to keep in mind that this string cannot be changed after initialization of browser. By setting this string, we give to browser information which connection it should search. This information will be provided by advertisers. There are two strict rules about naming: no longer, than 15 characters and it should contain lowercase ASCII characters, numbers and hyphens. Follow this rules otherwise runtime crash will occur.

Finaly advertiser:


override init() {
    ... 
    advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "appcoda-mpc")
    advertiser.delegate = self
}

Service type is the same as you may noticed. In addition, you may noticed discoveryInfo arguments. It has [String : String]? type and allow providing some information to other devices. It suppose a little bit more complex scenario that is why it is nil for simplicity.
So far init function of class should have the following appearance:


override init() {
    super.init()
 
    peer = MCPeerID(displayName: UIDevice.currentDevice().name)
 
    session = MCSession(peer: peer)
    session.delegate = self
 
    browser = MCNearbyServiceBrowser(peer: peer, serviceType: "appcoda-mpc")
    browser.delegate = self
 
    advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: "appcoda-mpc")
    advertiser.delegate = self
}

Implementing browsing

Now we can move to the main part. The following functions reflects key delegate implementations for Multipeer Connectivity lifecycle.


func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
    foundPeers.append(peerID) 
    // your code
}

This function called when some peer discovered. First, we keep it in peer array for further action. Next, you can handle event according to your project needs. For example, you can delegate further actions to some class where you can handle UI changes end etc.
If application can found peer, it can lose it as well. For this case, protocol contain:


func browser(browser: MCNearbyServiceBrowser!, lostPeer peerID: MCPeerID!) {
     // Removing losted peer from list.
     for (index, aPeer) in enumerate(foundPeers){
        if aPeer == peerID {
            foundPeers.removeAtIndex(index)
            break
        }
    } 
    // your code
}

It is impossible to imagine modern software development without error managing. Here is basic error handing example:


func browser(browser: MCNearbyServiceBrowser!, didNotStartBrowsingForPeers error: NSError!) {
    println(error.localizedDescription)
}

As we have connection functionality in separate file class we need to organize access to it from ViewController. Lets just create instance of ConnectionManager class in AppDelegate.swift that usually included by default in project. For example:


var connectionManager: ConnectionManager!

Don’t forget to initialize it:


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch. 
    connectionManager = ConnectionManager () 
    return true
}

Inside ViewController provide access to connectionManager:


let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate

If you did not catch why we need appDelegate the following code will clarify it. Inside target ViewController:


func viewDidLoad() {
    ... 
    appDelegate.connectionManager.browser.startBrowsingForPeers()
}

By implementing this code we make application start searching for others devices after UI has been loaded. The only thing to add here, stopBrowsingForPeers() will stop searching.

Implementing Advertising.

As you already understood, without advertising browsing is useless. In this example application will brows and advertise itself simultaneously. Starting advertising is as straight forward process as browsing:


appDelegate.connectionManager.advertiser.startAdvertisingPeer()

Function stopAdvertisingPeer() will stop process.

Invitation.

Goal of framework to help organize data exchange. Once device find other it is time to move to final step of first phase. In this example, invitation will be send automatically after device discovered by browser. However, in real project it is up to developer. In order to invite peer after it was discovered edit one of the delegate functions.


func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
    foundPeers.append(peerID) 
    let otherPeer = foundPeers.popLast()
    browser.invitePeer(otherPeer!, to: session, withContext: nil, timeout: 20)
}

What is going here? Function just extract last discovered peer and save it in variable (to be honest it is possible to put this inside invitePeer(), but it is a bit more readable isn’t it?). First invitePeer takes information about device which should be connected with current one. Next, we provide session object. Parameter withContext is nil. You can use to provide some extra information. It requires an NSData object. Finally, timeout define how much time inviter should wait for response.
Next steps will organize behavior on receivers side. It is time to implement MCNearbyServiceAdvertiserDelegate protocol and handle invitation. The following function called when application receiving infitation.


func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID: MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) {
    let invHandler = invitationHandler 
    invHandler(true, self.session)
}

Lets break down this function:

Application that receive invitation represented by advertiser . peerID refer to inviter, context server to provide additional information. According to official guidelines such data should be treated as untrusted. Key parameter here is invitationHandler, a block that your code must call to indicate whether the advertiser should accept or decline the invitation, and to provide a session with which to associate the peer that sent the invitation. In example above, invitation accepted immediately.

MCNearbyServiceAdvertiserDelegate protocol offer method to handle start advertising  failure:


func advertiser(advertiser: MCNearbyServiceAdvertiser!, didNotStartAdvertisingPeer error: NSError!) {
    println(error.localizedDescription)
}

The most basic thing you can do print error message into console.

Work with Session.

 

There was only preparations so far. Second phase is session itself. It has three possible states: Connected, Not Connected and Connecting. Last two refers to cases when user/application rejected connection or connection loosed and intermediate process that occurred before connected state.

It is turn of MCSessionDelegate  to be implemented.


func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) {
    switch state{
    case MCSessionState.Connected:
        println("Connected to session: \(session)")        
        // you can do something using peerID for example 
    case MCSessionState.Connecting:
        println("Connecting to session: \(session)")       
    default:
        println("Did not connect to session: \(session)")
    }
}

Sending data.

The following function is wrapper for standard sending operation. It is simple and handy example of data exchange organization. (inside connectionManager class)


func sendData(send data: Dictionary<String, String>, toPeer targetPeer: [MCPeerID]) -> Bool {
        let dataToSend = NSKeyedArchiver.archivedData(withRootObject: data)
        let peersArray = targetPeer        
        do {
_ = try session.send(dataToSend, toPeers: peersArray, with: MCSessionSendDataMode.unreliable)
        } catch {		
            return false
        }
        return true
    }

Framework accept information with NSData type for exchange. That is why there is wrapper. NSKeyedArchiver is solution here. In this example sendData accept Dictionary<String, String> but you can use anything that can be accepted by NSKeyedArchiver.archivedData(). According to definition of function send it takes array of peers. Pay attention if you need to have one receiver, prepare [MCPeerID] with one element not just MCPeerID. Transmission organized with very simple “try catch” construction to avoid unexpected surprises. MCSessionSendDataMode deserves special attention here. There are two modes. Officaial documentation states:

unreliable – “The framework should guarantee delivery of each message, enqueueing and retransmitting data as needed, and ensuring in-order delivery.”

reliable – “Messages to peers should be sent immediately without socket-level queuing. If a message cannot be sent immediately, it should be dropped. The order of messages is not guaranteed.”

 

For most cases, you can use unreliable mode. It is faster. However, if message delivery has crucial influence it is better to set reliable mode. Now in your ViewController you can send data using appDelegate.connectionManager.sendData().

Receiving data

This process provided by MPCSessionDelegate. Inside connectionManager just add:


func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) {
    // First of all “extract” information
    let extractedData = NSKeyedUnarchiver.unarchiveObject(with: data)
    let receivedData = extractedData as! Dictionary<String, String>
    // Now you can work with your receivedData
}

Important tips.

Framework is quite simple and convenient, isn’t it? Unfortunately, there are still some problems. First, work with many devices can be challenging. There is no exact number, but you may find that users of StackOverflow refer to 7 device per one session. That means that huge network require some tricks or searching for alternatives (for example https://github.com/jdiehl/async-network#request-based-networking).

According to official guidelines it preferable to do some UI changes in main thread. For example, lets consider the following scenario:


func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) {
    switch state{
    case MCSessionState.Connected:
        println("Connected to session: \(session)")        
        // some UI changes
 
    case MCSessionState.Connecting:
        println("Connecting to session: \(session)")
        // indicate it on your UI for example
    default:
        println("Did not connect to session: \(session)")
    }
}

You may want to use text label to indicate “status” of connection. All such action should be wrapped into next code:


DispatchQueue.main.async {
      // for example
       self.setStatus(status: "Connected")
}

Another advice, organize “keep alive” signal just to check connection. (Let us say, every 20 seconds). It may solve most connection lose issues.

Conclusion

MultipeerConnectivity framework is handy for performing basic network activates in your project. Of course, you will have to tune something to fit your projects necessities.
List of delegates function presented here is not full but enough for basic work. XCode will indicate about missed implementation but following hints will autocomplete missed function that you can leave empty or with minimal code if you don’t need them.

 

 

Comments are closed.