Mobile9-minute read

Collusion: Nearby Device Networking with MultipeerConnectivity in iOS

Traditionally, connecting devices for peer-to-peer communications has been a tedious task: An application needs to discover what’s around it, open connections on both sides, and then maintain them as network infrastructure, connections, distances, etc. Realizing the difficulties inherent in these activities, in iOS 7 and macOS 10.10 Apple introduced its MultipeerConnectivity framework.

Join Toptal Freelance iOS Developer Ben Gottlieb as he explains what MultipeerConnectivity is, and teaches us how to use it.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Traditionally, connecting devices for peer-to-peer communications has been a tedious task: An application needs to discover what’s around it, open connections on both sides, and then maintain them as network infrastructure, connections, distances, etc. Realizing the difficulties inherent in these activities, in iOS 7 and macOS 10.10 Apple introduced its MultipeerConnectivity framework.

Join Toptal Freelance iOS Developer Ben Gottlieb as he explains what MultipeerConnectivity is, and teaches us how to use it.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Ben Gottlieb
Verified Expert in Engineering

Ben has been an iOS developer for the entire life of the platform and has worked on mobile devices for more than 20 years.

Previously At

Salesforce
Share

Traditionally, connecting devices for peer-to-peer communications has been a bit of a heavy lift. An application needs to discover what’s around it, open connections on both sides, and then maintain them as network infrastructure, connections, distances, etc, all change. Realizing the difficulties inherent in these activities, in iOS 7 and macOS 10.10 Apple introduced its MultipeerConnectivity framework (henceforth MPC), designed to allow apps to perform these tasks with relatively low effort.

MPC takes care of much of the underlying required infrastructure here:

  • Multiple network interface support (Bluetooth, WiFi, and ethernet)
  • Device detection
  • Security via encryption
  • Small message passing
  • File transfer

In this article, we’ll mainly be addressing the iOS implementation, but most, if not all of this, is applicable to macOS and tvOS.

MultipeerConnectivity Session LifeCycle

There are numerous MultipeerConnectivity tutorials and examples out there that purport to walk iOS developers through the implementation of an MPC-based application. However, in my experience, they’re usually incomplete and tend to gloss over some important potential stumbling blocks with MPC. In this article, I hope to both walk the reader through a rudimentary implementation of such an app and call out areas where I’ve found it easy to get stuck.

Concepts & Classes

MPC is based on a handful of classes. Let’s walk through the list of the common ones, and build up our understanding of the framework.

  • MCSession – A session manages all communications between its associated peers. You can send messages, files, and streams via a session, and its delegate will be notified when one of these is received from a connected peer.
  • MCPeerID – A peer ID lets you identify individual peer devices within a session. It’s got a name associated with it, but be careful: peer IDs with the same name are not considered identical (see Ground Rules, below).
  • MCNearbyServiceAdvertiser – An advertiser allows you to broadcast your service name to nearby devices. This lets them connect to you.
  • MCNearbyServiceBrowser – A browser lets you search for devices using MCNearbyServiceAdvertiser. Using these two classes together allows you to discover nearby devices and create your peer-to-peer connections.
  • MCBrowserViewController – This provides a very basic UI for browsing nearby device services (vended via MCNearbyServiceAdvertiser). While suitable for some use cases, we won’t be using this, as, in my experience, one of the best aspects of MCP is its seamlessness.

Ground Rules

There are a couple of things to keep in mind when constructing an MPC network:

  • Devices are identified by MCPeerID objects. These are, superficially, wrapped strings, and in fact, can be initialized with simple names. Though two MCPeerIDs may be created with the same string, they are not identical. Thus, MCPeerIDs should never be copied or re-created; they should be passed around within the application. If necessary, they can be stored using an NSArchiver.
  • While the documentation on it is lacking, MCSession can be used to communicate between more than two devices. However, in my experience, the stablest way to utilize these objects is to create one for each peer your device is interacting with.
  • MPC will not work while your application is in the background. You should disconnect and tear down all of your MCSessions when your app is backgrounded. Don’t try and do more than minimal operations in any background tasks.

Getting Started with MultipeerConnectivity

Before we can establish our network, we need to do a little housekeeping, and then set up the advertiser and browser classes to discover other devices that we can communicate with. We’re going to create a singleton that we’ll use to hold a few state variables (our local MCPeerID and any connected devices), then we’ll create MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. These last two objects need a service type, which is just a string identifying your application. It needs to be less than 16 characters and should be as unique as possible (ie, “MyApp-MyCo”, not “Multipeer”). We can specify a (small) dictionary to our advertiser than browsers can read to give a bit more information when looking at nearby devices (perhaps a game type or device role).

Since MPC relies on system-provided APIs and correlates to real-world objects (other devices, as well as the shared “network” between them), it’s a good fit for the singleton pattern. While frequently overused, singletons are a good fit for shared resources such as this.

Here’s the definition of our singleton:


class MPCManager: NSObject {
  var advertiser: MCNearbyServiceAdvertiser!
  var browser: MCNearbyServiceBrowser!


  static let instance = MPCManager()
  
  let localPeerID: MCPeerID
  let serviceType = "MPC-Testing"
  
  var devices: [Device] = []
  
  override init() {
    if let data = UserDefaults.standard.data(forKey: "peerID"), let id = NSKeyedUnarchiver.unarchiveObject(with: data) as? MCPeerID {
      self.localPeerID = id
    } else {
      let peerID = MCPeerID(displayName: UIDevice.current.name)
      let data = try? NSKeyedArchiver.archivedData(withRootObject: peerID)
      UserDefaults.standard.set(data, forKey: "peerID")
      self.localPeerID = peerID
    }
    
    super.init()
    
    self.advertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: nil, serviceType: self.serviceType)
    self.advertiser.delegate = self
    
    self.browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: self.serviceType)
    self.browser.delegate = self
  }
}

Note that we’re storing our MCPeerID in user defaults (by way of NSKeyedArchiver), and re-using it. As mentioned above, this is important, and failure to cache it in some way can cause obscure bugs farther down the line.

Here’s our Device class, which we’ll use to keep track of what devices have been discovered, and what their state is:


class Device: NSObject {
  let peerID: MCPeerID
  var session: MCSession?
  var name: String
  var state = MCSessionState.notConnected
  
  init(peerID: MCPeerID) {
    self.name = peerID.displayName
    self.peerID = peerID
    super.init()
  }
  
  func invite() {
      browser.invitePeer(self.peerID, to: self.session!, withContext: nil, timeout: 10)
  }

}

Now that we’ve got our initial classes built out, it’s time to step back and think about the interplay between browsers and advertisers. In MPC, a device can advertise a service it offers, and it can browse for a service it’s interested in on other devices. Since we’re focused on device-to-device communication using just our app, we’ll both advertise and browse for the same service.

In a traditional client/server configuration, one device (the server) would advertise its services, and the client would browse for them. Since we’re egalitarian, we don’t want to have to specify roles for our devices; we’ll have every device both advertise and browse.

We need to add a method to our MPCManager to create devices as they’re discovered and track them in our devices array. Our method will take an MCPeerID, look for an existing device with that ID, and return it if found. If we don’t already have an existing device, we create a new one and add that to our device array.


func device(for id: MCPeerID) -> Device {
  for device in self.devices {
    if device.peerID == id { return device }
  }
  
  let device = Device(peerID: id)
  
  self.devices.append(device)
  return device
}

After a device has started advertising, another browsing device can attempt to attach to it. We’ll need to add delegate methods to our MPCSession class to handle incoming delegate calls from our advertiser in this case:


extension MPCManager: MCNearbyServiceAdvertiserDelegate {
  func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
    let device = MPCManager.instance.device(for: peerID)
    device.connect()
    invitationHandler(true, device.session)
  }
}

…a method on our Device to create the MCSession:


func connect() {
    if self.session != nil { return }
    
    self.session = MCSession(peer: MPCManager.instance.localPeerID, securityIdentity: nil, encryptionPreference: .required)
    self.session?.delegate = self
  }

…and finally a method to trigger the invitation when our browser discovers an advertiser:


extension MPCManager: MCNearbyServiceBrowserDelegate {
  func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
    let device = MPCManager.instance.device(for: peerID)
    device.invite(with: self.browser)
  }

Right now, we’re ignoring the withDiscoveryInfo argument; we could use this to filter out particular devices based on what they’ve made available (this is the same dictionary we supplied in the discoveryInfo argument to MCNearbyServiceAdvertiser, above).

Connecting Devices

Now that we’ve got all of our housekeeping taken care of, we can start the actual business of connecting devices.

In our MPCSession’s init method we set up both our advertiser and our delegate. When we’re ready to start connecting, we’ll need to start both of them up. This can be done in the App delegate’s didFinishLaunching method, or whenever is appropriate. Here’s the start() method we’ll add to our class:


func start() {
  self.advertiser.startAdvertisingPeer()
  self.browser.startBrowsingForPeers()
}

These calls will mean your app will begin broadcasting its presence over WiFi. Note that you don’t need to be connected to a WiFi network for this to work (but you do have to have it turned on).

When a device responds to an invitation and starts its MCSession, it will start receiving delegate callbacks from the session. We’ll add handlers for those to our device object; most of them we’ll ignore for the time being:


extension Device: MCSessionDelegate {
  public func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
    self.state = state
    NotificationCenter.default.post(name: Multipeer.Notifications.deviceDidChangeState, object: self)
  }
  
  public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { }
  
  public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { }
  
  public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { }

  public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { }

}

For the time being, we’re mainly concerned with the session(_:peer:didChangeState:) callback. This will be called whenever a device transitions to a new state (notConnected, connecting, and connected). We’ll want to keep track of this so that we can build a list of all connected devices:


extension MPCManager {
  var connectedDevices: [Device] {
    return self.devices.filter { $0.state == .connected }
  }
}

Sending Messages

Now that we’ve got all of our devices connected, it’s time to actually start sending messages back and forth. MPC offers three options in this regard:

  • We can send a block of bytes (a data object)
  • We can send a file
  • We can open a stream to the other device

For the sake of simplicity, we’ll only be looking at the first of these options. We’ll send simple messages back and forth, and not worry too much about the complexities of message types, formatting, etc. We’ll use a Codable structure to encapsulate our message, which will look like this:


struct Message: Codable {
  let body: String
}

We’ll also add an extension to Device to send one of these:


extension Device {
  func send(text: String) throws {
    let message = Message(body: text)
    let payload = try JSONEncoder().encode(message)
    try self.session?.send(payload, toPeers: [self.peerID], with: .reliable)
  }
}

~~~swift

Finally, we'll need to modify our `Device.session(_:didReceive:fromPeer)` code to receive the message, parse it, and notify any interested objects about it:

static let messageReceivedNotification = Notification.Name(“DeviceDidReceiveMessage”) public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { if let message = try? JSONDecoder().decode(Message.self, from: data) { NotificationCenter.default.post(name: Device.messageReceivedNotification, object: message, userInfo: [“from”: self]) } }


## Disconnections

Now that we've got a connection created between multiple devices, we have to be able to both disconnect on demand and also handle system interruptions. 

One of the undocumented weaknesses of MPC is that it doesn't function in the background. We need to observe the `UIApplication.didEnterBackgroundNotification` notification, and make sure that we shut down all our sessions. Failure to do this will lead to undefined states in the sessions and devices and can cause lots of confusing, hard-to-track-down errors. There is a temptation to use a background task to keep your sessions around, in case the user jumps back into your app. However, this is a bad idea, as MPC will usually fail within the first second of being backgrounded.

When your app returns to the foreground, you can rely on MPC's delegate methods to rebuild your connections.

In our MPCSession's `start()` method, we'll want to observe this notification and add code to handle it and shut down all our sessions.

~~~swift

func start() {
  self.advertiser.startAdvertisingPeer()
  self.browser.startBrowsingForPeers()
  
  NotificationCenter.default.addObserver(self, selector: #selector(enteredBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
}

@objc func enteredBackground() {
  for device in self.devices {
    device.disconnect()
  }
}

func disconnect() {
	self.session?.disconnect()
	self.session = nil
}

Conclusions

This article covers the architecture required to build out the networking components of a MultipeerConnectivity based application. The full source code (available on Github) offers a minimal user-interface wrapper that allows you to view connected devices, and send messages between them.

MPC offers near-seamless connectivity between nearby devices without needing to worry about WiFi networks, Bluetooth, or complex client/server gymnastics. Being able to quickly pair up a few phones for a short gaming session, or connect two devices for sharing, is done in typical Apple fashion.

The source code for this project is available on Github at https://github.com/bengottlieb/MultipeerExample.

Designing an iOS that uses AFNetworking? The Model-View-Controller (MVC) design pattern is great for a maintainabe codebase, but sometimes you need a single class to handle your networking due to concerns like DRY code, centralized networking-logging and, especially, rate-limiting. Read all about handling this with a Singleton Class in iOS Centralized and Decoupled Networking: AFNetworking Tutorial with a Singleton Class

Understanding the basics

  • What is a peer-to-peer application?

    A networked app that is able to connect to other instances (peers) without needing a server or other intermediary.

  • If your data is sensitive (such as shared files), you may want to prevent others from intercepting it. For many peer-to-peer applications, however, it’s not required (think: game moves or simple instructions).

  • When two devices are connected, they communicate via messages sent back and forth.

  • In a traditional client-server network, the infrastructure includes the server and the connection equipment (e.g. a WiFi base station). In a peer-to-peer model, that can be reduced to a network consisting of just the devices themselves and an ad-hoc connection (no external hardware, just the devices’ own radios).

  • Peer-to-peer communication involves devices on a network talking directly to each other, without going through an intermediary server.

  • Peer-to-peer networks can be set up anywhere there are two or more capable devices, without having to involve a third, potentially un-reachable, server. For example, when there’s no connection to the internet, but the devices can still see each other.

  • MPC takes care of a lot of the underlying setup and maintenance tasks, such as peer discovery and low-level communications. It saves the developer a lot of time and effort by handling common functions for them.

  • There are a few not-well-documented weak spots that, if not guarded against, can cause connection difficulties. There is also a general loss of flexibility that comes with a general purpose framework. For most uses, however, MPC is well suited.

Hire a Toptal expert on this topic.
Hire Now
Ben Gottlieb

Ben Gottlieb

Verified Expert in Engineering

Chicago, IL, United States

Member since June 11, 2018

About the author

Ben has been an iOS developer for the entire life of the platform and has worked on mobile devices for more than 20 years.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Salesforce

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.