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:

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

Advertising the Service

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

  1. Create a new Swift class ColorService for all the connectivity code.

  2. 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)")
        }
        
    }
  3. Create a ColorService instance in the controller implementation:

    class ColorSwitchViewController: UIViewController {
    
        let colorService = ColorService()
        
        // ...
        
    }
  4. Run the app (simulator or device).

  5. 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:

    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

  1. 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.

  1. 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")
        }
    
    }
  2. 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.

  3. 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

  1. 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)
        
    }
  2. 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)")
                }
            }
    
        }
    
    }
  3. 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

  1. 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)")
                }
            }
        }
    
    }
  2. Run the updated app on two devices and test the connection:

    Multipeer Connectivity

More information

Btn download 3c20f11b8e Download the finished example project
Btn read 3c0e607615 iOS Developer Blog
Btn subscribe 930758687e Subscribe: Email · Twitter
Btn training bbbdf557d2 Next iOS training: 10. - 14. September 2018, München
Btn about 5378472193 About me · Contact