Send data between Apple devices (iPhone/iPad/macOS) using Multipeer Connectivity

Today I want to introduce an Apple framework, called Multipeer Connectivity.

“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.”

Reference here: https://developer.apple.com/documentation/multipeerconnectivity

With these few phrases, you can imagine the potential behind this framework and the applications that can be done…

How it works

To make it simpler, we can imagine Airdrop:

you choose something to send (image, text, file, streaming,…)

open AirDrop

Search for users

select a user

send data to the selected user

the selected peer accept the transfer (the invitation)

data is transferred.

Using Multipeer Connectivity, we can add some frameworks objects to the same bullet points:

you choose something to send (image, text, file, streaming,…)

open AirDrop -> initialize the MCSession

Search for users -> initialize the MCNearbyServiceBrowser and the MCNearbyServiceAdvertiser

select a user -> MCPeerID

send data to the selected peer -> MCSession.send()

the selected peer accept the transfer (the invitation) -> MCNearbyServiceBrowser.invitePeer()

data is transferred -> MCSessionDelegate.didReceive()

The implementation is relatively simple.

Show me the code!

Initialize the session, the browser and the advertiser:

// helper initializer
public init( delegate: MPCManagerDelegate, peerId: String = UIDevice.current.name ) {
super.init()

self.delegate = delegate
self.peer = MCPeerID(displayName: peerId)

initSession(peer: peer!, delegate: self)
initBrowser(peer: peer!, delegate: self, serviceType: serviceType)
initAdvertiser(peer: peer!, delegate: self, serviceType: serviceType)
}

// session initializer
fileprivate func initSession( peer: MCPeerID, delegate: MCSessionDelegate ) {
session = MCSession(peer: peer, securityIdentity: nil, encryptionPreference: .required)
session.delegate = delegate
}

// browser initializer
fileprivate func initBrowser( peer: MCPeerID, delegate: MCNearbyServiceBrowserDelegate, serviceType: String ) {
browser = MCNearbyServiceBrowser(peer: peer, serviceType: serviceType)
browser.delegate = delegate
}

// advertiser initializer
fileprivate func initAdvertiser( peer: MCPeerID, delegate: MCNearbyServiceAdvertiserDelegate, serviceType: String ) {
advertiser = MCNearbyServiceAdvertiser(peer: peer, discoveryInfo: nil, serviceType: serviceType)
advertiser.delegate = delegate
}

Well, the init is complete. Now we need to implements all the delegates, like: session.delegate, browser.delegate and advertiser.delegate.

MCSessionDelegate

Implements the MCSessionDelegate mandatory delegates:

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {}

func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}

The second one is very important because is the callback of the data reception.

MCNearbyServiceBrowserDelegate

func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {}

func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {}

We can use the foundPeer delegate to add a peer in the list of founded peers (for any use).

MCNearbyServiceAdvertiserDelegate

In the Advertiser we manage the invitation.

func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {}

Well, initialization is complete, delegates are created, we can start the scan.

browser.startBrowsingForPeers()
advertiser.startAdvertisingPeer()

To test your code I suggest using a simulator and device (both running in debug) or two simulators, but you can debug only one each time.

Real life application: a simple chat

Using this framework you can chat with phones / ipads / mac around you… so why do not creating an “offline” chat?

Let’s do it!

The first step is to create a new XCode project and create an interface like this. Similar of course…

It should be a simple chat interface, with a UITextfield, a UIButton and a UITableView with two types of UITableViewCells (one for the sender and one for the receiver).

Connect all the IBOutlets and continue reading when you’re ready.

Next, create a new Message object:

struct Message {
var sender: String
var message: String
}

and a local array of type Message in your ViewController to use later in the table view:

lazy var messages: [Message] = [] // the table view rows

Copy / paste the two helper function to send data to peers:

open func sendData(dictionaryWithData message: Message, toPeer targetPeer: [MCPeerID]) -> Bool {
do {
let dictionary: [String: String] = ["message": message.message]
if let dataToSend: Data = try archivedData(dictionaryWithData: dictionary) {
try session.send(dataToSend, toPeers: targetPeer, with: .reliable)
}
return true
} catch {}
return false
}

private func archivedData( dictionaryWithData dictionary: Dictionary<String, String> ) throws -> Data? {
return try NSKeyedArchiver.archivedData(withRootObject: dictionary, requiringSecureCoding: true)
}

The session.send() accept a Data object. We can handle it now using the Message struct created before and convert it to a NSKeyedArchiver object.

In this way you can send data to one or more MCPeerID.

But how to get the peers?

To do this you can add peers in your list of discovered peers using the delegates of MCNearbyServiceBrowserDelegate.

You can manage this for instance in the browser:foundPeer:withDiscoveryInfo callback.

If you want to send data to the peer(s) you must invite them:

browser.invitePeer(peerID, to: session, withContext: nil, timeout: timeout)

Last thing is to manage the data received.

To do this, go in the MCSessionDelegate methods and send a notification using the NotificationCenter to inform the app that you receive something or implement the design pattern you prefer for this.

Example:

open func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
let dictionary: [String: Any] = ["data": data, "fromPeer": peerID]
NotificationCenter.default.post(name: Notifications.kReceiveData.name, object: dictionary)
}

In the ViewController, implement the notification observer and refresh the table when data change:

Example:

NotificationCenter.default.addObserver(
self,
selector: #selector(handleReceivedData(notification:)),
name: Notifications.kReceiveData.name,
object: nil)

@objc func handleReceivedData(notification: NSNotification) {
// refresh table with new message(s) received
}

You should see something like this:

Your “offline” chat is ready. Offline means that you don’t need to register to any web services.

You can exchange Data using Wifi / Bluetooth and peer-to-peer network.

Repeat, you can send a Data object, so you can send everything!

For the lazy var, the code is here: https://github.com/elpsk/MultipeerConnectivity-Example.

You should implement by yourself the invitation, the disconnection, and all the features available for this beautiful framework.

The source code not contain this, is just a simple example.

Enjoy peer!