iOS & Swift Tutorial: Multipeer Connectivity

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

The Multipeer Connectivity framework was added in iOS 7 and 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:

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 the Multipeer Connectivity, the app has to advertise a service using the MCNearbyServiceAdvertiser class.

  1. Create a new Swift class ColorServiceManager 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 ColorServiceManager : 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 ColorServiceManager : 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 ColorServiceManager instance in the controller implementation:

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

  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 ColorServiceManager : 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 ColorServiceManager : 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 ColorServiceManager : 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. On 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 ColorServiceManager : NSObject {
        
        // ...
            
        lazy var session : MCSession = {
            let session = MCSession(peer: self.myPeerId, securityIdentity: nil, encryptionPreference: .required)
            session.delegate = self
            return session
        }()
        
    }
    
    // ...
    
    extension ColorServiceManager : MCSessionDelegate {
    
        func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
            NSLog("%@", "peer \(peerID) didChangeState: \(state)")
        }
    
        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 ColorServiceManager : 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 ColorServiceManager : 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 ColorServiceManagerDelegate to notify the UI about service events:

    protocol ColorServiceManagerDelegate {
        
        func connectedDevicesChanged(manager : ColorServiceManager, connectedDevices: [String])
        func colorChanged(manager : ColorServiceManager, colorString: String)
        
    }
  2. Declare a delegate property for the ColorServiceManager and implement a method sendColor that uses the session method sendData to send data to connected peers:

    class ColorServiceManager : NSObject {
        
        // ...
        
        var delegate : ColorServiceManagerDelegate?
        
        // ...
    
        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 ColorServiceManager : 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 ColorServiceManager and handle the events by updating the UI:

    class ColorSwitchViewController: UIViewController {
    
        @IBOutlet weak var connectionsLabel: UILabel!
    
        let colorService = ColorServiceManager()
    
        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")
        }
    
        func change(color : UIColor) {
        UIView.animate(withDuration: 0.2) {
            self.view.backgroundColor = color
        }
    }
    
    }
    
    extension ColorSwitchViewController : ColorServiceManagerDelegate {
    
        func connectedDevicesChanged(manager: ColorServiceManager, connectedDevices: [String]) {
            OperationQueue.main.addOperation {
                self.connectionsLabel.text = "Connections: \(connectedDevices)"
            }
        }
    
        func colorChanged(manager: ColorServiceManager, 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 and test the app:

    Multipeer Connectivity

More information

Example code

You can download the completed example project here: ConnectedColors.zip.

Share
Twitter Facebook
website screenshot mac os x
Ralf Ebert Ralf Ebert is an independent software developer and trainer for Mac OS X and iOS. He makes the Page Layers and Straight ahead apps and conducts iOS trainings in Germany since 2009.