Multipeer Сonnectivity Framework in Swift: tutorial

This article is about Multipeer connectivity, Swift Framework developed by Apple. Basically it is a way to connect two or more devices nearby directly one to another, through Bluetooth or Wi-Fi (including Wi-Fi Direct). You can use this framework for building simple games or a chat, for example, so that you can be connected to people around you even when the centralised network is off.

We will try to present you a simple way to implement basic multipeer connectivity in your project and tell what problems we met while working with it. First of all, do not think of it as about something very complicated for developer; like many other Apple’s native frameworks, it works “out of the box”.

Let’s start.

If you don’t want to go through the whole tutorial, just read working code, go to gitlab project and download it — https://gitlab.com/kolotilinma/tictactoe.git.

First, you need to import MultipeerConnectivity; then make your ViewController class to adapt protocols, and declare some variables:

import MultipeerConnectivityclass ViewController: UITableViewController, MCSessionDelegate, MCBrowserViewControllerDelegate {  var peerID: MCPeerID!
var mcSession: MCSession!
var mcAdvertiserAssistant: MCAdvertiserAssistant!
...
}

MCSession is the manager class that handles all multipeer connectivity for us. MCPeerID uniquely identifies a user within a session. MCAdvertiserAssistant is used for creating a session, telling others that we exist and handling invitations. MCBrowserViewController is used when looking for sessions, showing users who is nearby and letting them join.

Inside viewDidLoad method we assign the following values:

override func viewDidLoad() {
...
peerID = MCPeerID(displayName: UIDevice.current.name)
mcSession = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
mcSession.delegate = self
}

Now our device has an Id taken from Settings; and an instance of MCSession class is created to be able to use multipeer connectivity.

Adopted protocols require some methods — you can either insert them automatically or copy from the following:

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {}func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {}func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, 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 browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {}func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {}

For now, you can leave most of this methods empty. Just follow the tutorial further.

MCSessionState allows you to see if the connection status has changed. It has several properties that you can check. The most common way would be to use switch statement

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
switch state {
case .connected: print ("connected \(peerID)")
case .connecting: print ("connecting \(peerID)")
case .notConnected: print ("not connected \(peerID)")
default: print("unknown status for \(peerID)")
}
}

For the MCBrowserViewController methods we need to add some code:

func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {dismiss(animated: true)}func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {dismiss(animated: true)}

Now, after implementing needed methods, we need to initiate the process of connectivity. In this tutorial we suppose you use buttons and alert controllers.

Initiating the process

The system works like this: one device starts hosting a session and becomes visible for others by its name. Another device can join the session, find the first one in the list and ask for confirmation from the host.

To start, we put a simple alert controller asking if we want to host or join a session. For example, you use a button, connect it to ViewController and create an UIAlertController for it:

@IBAction func buttonPressed(_ sender: UIButton) {  let ac = UIAlertController(title: “Connect to others”, message: nil, preferredStyle: .alert)  ac.addAction(UIAlertAction(title: “Host a session”, style: .default) {//here we will add a closure to host a session
})
ac.addAction(UIAlertAction(title: “Join a session”, style: .default) {//here we will add a closure to join a session
})
ac.addAction(UIAlertAction(title: “Cancel”, style: .cancel)) present(ac, animated: true)}

Alert action to host a session may use following closure to start advertising itself as a host: here we create a advertiser assistant, that starts telling ios devices aroung about working session.

{ (UIAlertAction) in 
self
.mcAdvertiserAssistant = MCAdvertiserAssistant(serviceType: "todo", discoveryInfo: nil, session: self.mcSession)
self.mcAdvertiserAssistant.start()
}

Pay attention: serviceType parameter should be a short text string that describes the app’s networking protocol. According to Apple, it should be:

  • Must be 1–15 characters long
  • Can contain only ASCII lowercase letters, numbers, and hyphens
  • Must contain at least one ASCII letter
  • Must not begin or end with a hyphen
  • Must not contain hyphens adjacent to other hyphens.

This name should be easily distinguished from unrelated services. For example, a text chat app made by ABC company could use the service type abc-textchat.¹

While joining a session closure may look like this: we show a browser with a list of possible connections and make our view controller delegate to this object.

{ (UIAlertAction) in
let
mcBrowser = MCBrowserViewController(serviceType: “todo”, session: self.mcSession)
mcBrowser.delegate = self
self
.present(mcBrowser, animated: true, completion: nil)
}

Now you can try and start your app on two devices simultaneously (or on a simulator and a physical device).

You have followed the tutorial, but still can’t get alert to join a session from another device? Well, that is a common problem that appeared in XCode 11.2 and iOS 13 and still exists. We don’t know if Apple find the framework obsolete or will fix it later.

One of the solutions can be — downgrade from using SceneDelegate and AppDelegate to only one class: AppDelegate and leave using UIScene for future apps, not this one.

  • first, completely remove the “Application Scene Manifest” entry from Info.plist;
  • remove the SceneDelegate class, and remove all scene related methods in your AppDelegate;
  • if missing, add the property var window: UIWindow? to your AppDelegate class.

Your app should now only use the app delegate and under iOS 13 it will have the same life cycle as iOS 12. And Multipeer connectivity should work as it is originally designed.

Sending and receiving information

The only thing left — is how to send information itself from one device to another? For that we use the following method. You need to determine what data you need to share and when: after pressing some button or just after establishing the connection. In the required place add following code:

if mcSession.connectedPeers.count > 0 {
if let safeData = // you data that needs to be sent
{do
{
try mcsession.send(safeData, toPeers: mcsession.connectedPeers, with: .reliable)
} catch let error as NSError {
print("\(error.localizedDescription)")
}
}
}

Your data should be formatted to the type Data(). For images, for example, easy way to do it would be to use the .pngData() method.

Last thing that we need to do — give the receiving device understand what to do with received data and what to do next. In this situation it depends on what data you tried to transfer. In anyway, use following method (it is one of the methods added while adapting protocols):

func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { ... }

Remember, that you receive the object of type Data(). And you may need to decode it back to needed type. Then update the UI. Don’t forget to execute it on the main thread. The example follows (also from the protocol methods):

func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
self.images.insert(image, at: 0)
self.collectionView.reloadData()
}
}
}

Hope you enjoyed reading our article and found it useful!

For any questions please get in touch or write a comment.

This article is collaborative work of Conjunctivity team from Apple Developer Academy in Naples, including:

--

--