Multipeer Connectivity

This tutorial shows how to use the Multipeer Connectivity framework to communicate between iOS devices:

The Multipeer Connectivity framework provides a layer on top of the Bonjour protocol. You can communicate with apps running on nearby devices. Under the hood, the framework automatically chooses a suitable networking technology:

Wi-Fi network if both devices are on the same Wi-Fi network

Peer-to-peer Wi-Fi (supported on iOS devices with a Lighting Connector and on Macs since 2012)

(supported on iOS devices with a Lighting Connector and on Macs since 2012) Bluetooth

Last update: July 25, 2018 | Tested with: Xcode 9.4

Requirements

This is a tutorial for advanced iOS developers. It requires practical Swift programming skills and good knowledge about iOS development.

Project setup

Download the starter project for the tutorial here: ConnectedColors-starter.zip or, alternatively, create a new Single View application “ConnectedColors” and setup a view controller with a label for the connected devices and two buttons to switch the color. Create a view controller class ColorSwitchViewController, create an IBOutlet for the label and IBAction methods for the color buttons:

Advertising the Service

To communicate using Multipeer Connectivity, the app has to advertise a service using the MCNearbyServiceAdvertiser class.

Create a new Swift class ColorService for all the connectivity code. Import the MultipeerConnectivity module.

Define a constant for the service type that will identify the service uniquely.

Create a MCPeerID - the displayName will be visible to other devices.

Instantiate a MCNearbyServiceAdvertiser to advertise the service.

Implement the MCNearbyServiceAdvertiserDelegate protocol and log the delegate events.

Start advertising the service when the object is created and stop advertising when the object is destroyed: import Foundation import MultipeerConnectivity class ColorService : NSObject { // Service type must be a unique string, at most 15 characters long // and can contain only ASCII lowercase letters, numbers and hyphens. private let ColorServiceType = "example-color" private let myPeerId = MCPeerID ( displayName : UIDevice . current . name ) private let serviceAdvertiser : MCNearbyServiceAdvertiser override init () { self . serviceAdvertiser = MCNearbyServiceAdvertiser ( peer : myPeerId , discoveryInfo : nil , serviceType : ColorServiceType ) super . init () self . serviceAdvertiser . delegate = self self . serviceAdvertiser . startAdvertisingPeer () } deinit { self . serviceAdvertiser . stopAdvertisingPeer () } } extension ColorService : MCNearbyServiceAdvertiserDelegate { func advertiser ( _ advertiser : MCNearbyServiceAdvertiser , didNotStartAdvertisingPeer error : Error ) { NSLog ( "%@" , "didNotStartAdvertisingPeer: \( error ) " ) } func advertiser ( _ advertiser : MCNearbyServiceAdvertiser , didReceiveInvitationFromPeer peerID : MCPeerID , withContext context : Data ?, invitationHandler : @ escaping ( Bool , MCSession ?) -> Void ) { NSLog ( "%@" , "didReceiveInvitationFromPeer \( peerID ) " ) } } Create a ColorService instance in the controller implementation: class ColorSwitchViewController : UIViewController { let colorService = ColorService () // ... } Run the app (simulator or device). Check that the service is advertised on the local network either using dns-sd: $ dns-sd -B _services._dns-sd._udp Browsing for _services._dns-sd._udp DATE: ---Fri 10 Feb 2017--- 21:35:17.120 ...STARTING... Timestamp A/R Flags if Domain Service Type Instance Name 21:35:54.043 Add 2 4 . _tcp.local. _example-color or using the Bonjour Browser: (Thanks: Wes Campaigne’s answer to the Stack Exchange question Can I list all the Bonjour-enabled services that are running?)

Scanning for the advertised service

Create an MCNearbyServiceBrowser to scan for the advertised service on other devices.

Implement the MCNearbyServiceBrowserDelegate protocol and log all the browser events: import Foundation import MultipeerConnectivity class ColorService : NSObject { // Service type must be a unique string, at most 15 characters long // and can contain only ASCII lowercase letters, numbers and hyphens. private let ColorServiceType = "example-color" private let myPeerId = MCPeerID ( displayName : UIDevice . current . name ) private let serviceAdvertiser : MCNearbyServiceAdvertiser private let serviceBrowser : MCNearbyServiceBrowser override init () { self . serviceAdvertiser = MCNearbyServiceAdvertiser ( peer : myPeerId , discoveryInfo : nil , serviceType : ColorServiceType ) self . serviceBrowser = MCNearbyServiceBrowser ( peer : myPeerId , serviceType : ColorServiceType ) super . init () self . serviceAdvertiser . delegate = self self . serviceAdvertiser . startAdvertisingPeer () self . serviceBrowser . delegate = self self . serviceBrowser . startBrowsingForPeers () } deinit { self . serviceAdvertiser . stopAdvertisingPeer () self . serviceBrowser . stopBrowsingForPeers () } } extension ColorService : MCNearbyServiceAdvertiserDelegate { func advertiser ( _ advertiser : MCNearbyServiceAdvertiser , didNotStartAdvertisingPeer error : Error ) { NSLog ( "%@" , "didNotStartAdvertisingPeer: \( error ) " ) } func advertiser ( _ advertiser : MCNearbyServiceAdvertiser , didReceiveInvitationFromPeer peerID : MCPeerID , withContext context : Data ?, invitationHandler : @ escaping ( Bool , MCSession ?) -> Void ) { NSLog ( "%@" , "didReceiveInvitationFromPeer \( peerID ) " ) } } extension ColorService : MCNearbyServiceBrowserDelegate { func browser ( _ browser : MCNearbyServiceBrowser , didNotStartBrowsingForPeers error : Error ) { NSLog ( "%@" , "didNotStartBrowsingForPeers: \( error ) " ) } func browser ( _ browser : MCNearbyServiceBrowser , foundPeer peerID : MCPeerID , withDiscoveryInfo info : [ String : String ]?) { NSLog ( "%@" , "foundPeer: \( peerID ) " ) } func browser ( _ browser : MCNearbyServiceBrowser , lostPeer peerID : MCPeerID ) { NSLog ( "%@" , "lostPeer: \( peerID ) " ) } }

Sending and accepting invitations

All devices will advertise the service and scan for the service at the same time. Since iOS 8 this is supported - you can invite any peer you detect while browsing and the framework will handle simultaneous invites.

Create a lazy initialized session property to create a MCSession on demand and implement the MCSessionDelegate protocol: class ColorService : NSObject { // ... lazy var session : MCSession = { let session = MCSession ( peer : self . myPeerId , securityIdentity : nil , encryptionPreference : . required ) session . delegate = self return session }() } // ... extension ColorService : MCSessionDelegate { func session ( _ session : MCSession , peer peerID : MCPeerID , didChange state : MCSessionState ) { NSLog ( "%@" , "peer \( peerID ) didChangeState: \( state . rawValue ) " ) } func session ( _ session : MCSession , didReceive data : Data , fromPeer peerID : MCPeerID ) { NSLog ( "%@" , "didReceiveData: \( data ) " ) } func session ( _ session : MCSession , didReceive stream : InputStream , withName streamName : String , fromPeer peerID : MCPeerID ) { NSLog ( "%@" , "didReceiveStream" ) } func session ( _ session : MCSession , didStartReceivingResourceWithName resourceName : String , fromPeer peerID : MCPeerID , with progress : Progress ) { NSLog ( "%@" , "didStartReceivingResourceWithName" ) } func session ( _ session : MCSession , didFinishReceivingResourceWithName resourceName : String , fromPeer peerID : MCPeerID , at localURL : URL ?, withError error : Error ?) { NSLog ( "%@" , "didFinishReceivingResourceWithName" ) } } In the MCNearbyServiceBrowserDelegate, invite any peer that is discovered: extension ColorService : MCNearbyServiceBrowserDelegate { // ... func browser ( _ browser : MCNearbyServiceBrowser , foundPeer peerID : MCPeerID , withDiscoveryInfo info : [ String : String ]?) { NSLog ( "%@" , "foundPeer: \( peerID ) " ) NSLog ( "%@" , "invitePeer: \( peerID ) " ) browser . invitePeer ( peerID , to : self . session , withContext : nil , timeout : 10 ) } } Note: This code invites any peer automatically. The MCBrowserViewController class could be used to scan for peers and invite them manually. When you receive an invitation, accept it by calling the invitionHandler block with true: extension ColorService : MCNearbyServiceAdvertiserDelegate { // ... func advertiser ( _ advertiser : MCNearbyServiceAdvertiser , didReceiveInvitationFromPeer peerID : MCPeerID , withContext context : Data ?, invitationHandler : @ escaping ( Bool , MCSession ?) -> Void ) { NSLog ( "%@" , "didReceiveInvitationFromPeer \( peerID ) " ) invitationHandler ( true , self . session ) } } Note: This code accepts all incoming connections automatically. This would be like a public chat and you need to be very careful to check and sanitize any data you receive over the network as you cannot trust the peers.

To keep sessions private the user should be notified and asked to confirm incoming connections. This can be implemented using the MCAdvertiserAssistant classes.

Sending and receiving color values

Declare a delegate protocol ColorServiceDelegate to notify the UI about service events: protocol ColorServiceDelegate { func connectedDevicesChanged ( manager : ColorService , connectedDevices : [ String ]) func colorChanged ( manager : ColorService , colorString : String ) } Declare a delegate property for the ColorService and implement a method sendColor that uses the session method sendData to send data to connected peers: class ColorService : NSObject { // ... var delegate : ColorServiceDelegate ? // ... func send ( colorName : String ) { NSLog ( "%@" , "sendColor: \( colorName ) to \( session . connectedPeers . count ) peers" ) if session . connectedPeers . count > 0 { do { try self . session . send ( colorName . data ( using : . utf8 ) ! , toPeers : session . connectedPeers , with : . reliable ) } catch let error { NSLog ( "%@" , "Error for sending: \( error ) " ) } } } } Extend the implementation of the MCSessionDelegate protocol so that delegate is notified when the connected devices change or when data is received: extension ColorService : MCSessionDelegate { // ... func session ( _ session : MCSession , peer peerID : MCPeerID , didChange state : MCSessionState ) { NSLog ( "%@" , "peer \( peerID ) didChangeState: \( state . stringValue ()) " ) self . delegate ?. connectedDevicesChanged ( manager : self , connectedDevices : session . connectedPeers . map { $0 . displayName }) } func session ( _ session : MCSession , didReceive data : Data , fromPeer peerID : MCPeerID ) { NSLog ( "%@" , "didReceiveData: \( data . length ) bytes" ) let str = String ( data : data , encoding : . utf8 ) ! self . delegate ?. colorChanged ( manager : self , colorString : str ) } }

Updating the UI

In ColorSwitchViewController, register as delegate to the ColorService and handle the events by updating the UI: class ColorSwitchViewController : UIViewController { // ... override func viewDidLoad () { super . viewDidLoad () colorService . delegate = self } @IBAction func redTapped () { self . change ( color : . red ) colorService . send ( colorName : "red" ) } @IBAction func yellowTapped () { self . change ( color : . yellow ) colorService . send ( colorName : "yellow" ) } // ... } extension ColorSwitchViewController : ColorServiceDelegate { func connectedDevicesChanged ( manager : ColorService , connectedDevices : [ String ]) { OperationQueue . main . addOperation { self . connectionsLabel . text = "Connections: \( connectedDevices ) " } } func colorChanged ( manager : ColorService , colorString : String ) { OperationQueue . main . addOperation { switch colorString { case "red" : self . change ( color : . red ) case "yellow" : self . change ( color : . yellow ) default : NSLog ( "%@" , "Unknown color value received: \( colorString ) " ) } } } } Run the updated app on two devices and test the connection:

More information

Example project