Building Peer-to-Peer Sessions: Advertising and Browsing Devices

Building Peer-to-Peer Sessions: Advertising and Browsing Devices

Understand how to advertise a session and browse for devices with the Multipeer Connectivity Framework.

For devices to discover and connect, one possible technology you can use within the Apple ecosystem is the Multipeer Connectivity framework.

To have an introduction to the Multipeer Connectivity framework, take a look at the Getting Started with Multipeer Connectivity in Swift reference article.

Focusing on discovering and being discovered by other devices, this process is accomplished through two complementary roles:

  • advertising to let other devices know that it is available for connection, and
  • browsing to advertise the same service when searching for devices.

Adding Permissions

To guarantee that the app can use multipeer connectivity properly, it needs the following permissions to be added in the Info tab of the project settings:

Setting up the device ID

For this example, let’s create a class that will be responsible for managing the peer-to-peer sessions, called PeerSessionManager.

import Foundation
import MultipeerConnectivity

class PeerSessionManager: NSObject, ObservableObject {
    override init() {
    
    }
}

Each user device needs a MCPeerID with the name that other devices will see when connecting, for example, "Gabriel's iPhone". It also needs a serviceID that ensures only devices with the same ID are discovered and connected. Add the following properties to the session manager class:

// Getting the user device name to show during the discovery
private let peerID = MCPeerID(displayName: UIDevice.current.name)

// Setting up the service name
private let serviceID = "p2p-demo"

The core of peer-to-peer communication is the MCSession object. Add a property to store the session object and initialize it in the class constructor.

Even though we won't be sending any data yet, set the encryption preference of the session to be required to ensure all communication is encrypted.

class PeerSessionManager: NSObject, ObservableObject {
    private let peerID = MCPeerID(displayName: UIDevice.current.name)
    private let serviceID = "p2p-demo"
    
    private var session: MCSession
    
    override init() {
        session = MCSession(
            peer: peerID,
            securityIdentity: nil,
            encryptionPreference: .required
        )
    }   
}

Setting up the session delegate

To track state changes on a session, we need to implement the MCSessionDelegate. Start by extending the PeeSessionManager class and conforming to the MCSessionDelegate protocol.

extension PeerSessionManager: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        // Future implementation will be here!
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        // TODO
    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        // TODO
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        // TODO
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) {
        // TODO
    }
}

Then set up the delegate of the session in the class initializer.

override init() {
    session = MCSession(
        peer: peerID,
        securityIdentity: nil,
        encryptionPreference: .required
    )
    
    // Call the initializer of the parent class
    super.init()

    // Set the delegate of the session object
    session.delegate = self
}

Making the device visible in the network

To initiate the process of making a device visible on the local network, use MCNearbyServiceAdvertiser and pass the peerID and serviceID as parameters.

Add a property to store the advertiser object and initialize it in the class constructor.

class PeerSessionManager: NSObject, ObservableObject {

    // Other properties from previous steps

    // Property for the advertiser
    private var advertiser: MCNearbyServiceAdvertiser
    
    override init() {
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
        
        // Initialize the advertiser
        advertiser = MCNearbyServiceAdvertiser(
            peer: peerID,
            discoveryInfo: nil,
            serviceType: serviceID
        )
        
        super.init()
        
        session.delegate = self
        // Set up the advertiser delegate
        advertiser.delegate = self
    }
}

The delegate MCNearbyServiceAdvertiserDelegate handles incoming invitations when another peer attempts to connect.

Extend the PeeSessionManager class and conform to the MCNearbyServiceAdvertiserDelegate protocol.

extension PeerSessionManager: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
    }
}

The method is triggered when another peer tries to connect to your advertisement. In the example, the connection will be automatically accepted by invitationHandler(true, session), but you can also apply acceptance rules.

Searching for other devices

The action of searching for which devices are available comes from the MCNearbyServiceBrowser class, to search for peers that are advertising the same serviceID.

The MCNearbyServiceBrowserDelegate delegate will inform you when a peer is found or lost.

Like in the previous step, add a property to store the browser object and initialize it in the class constructor. Additionally, create two properties to store the discovered and connected peers.

class PeerSessionManager: NSObject, ObservableObject {

    // Other properties from previous steps

    // Property for the browser
    private var browser: MCNearbyServiceBrowser
    
    // Properties to store the discovered and connected peers
    @Published var discoveredPeers: [MCPeerID] = []
    @Published var connectedPeers: [MCPeerID] = []
    
    override init() {
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceID)
        
        // Initialize the browser
        browser = MCNearbyServiceBrowser(
            peer: peerID,
            serviceType: serviceID
        )
        
        super.init()
        
        session.delegate = self
        advertiser.delegate = self
        // Set up the browser delegate
        browser.delegate = self
    }
}

Then, extend the PeeSessionManager class and conform to the MCNearbyServiceAdvertiserDelegate protocol, implementing the browsing methods to store the discovered peers.

extension PeerSessionManager: MCNearbyServiceBrowserDelegate {

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        DispatchQueue.main.async {
            if !self.discoveredPeers.contains(peerID) {
                self.discoveredPeers.append(peerID)
            }
        }
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        DispatchQueue.main.async {
            self.discoveredPeers.removeAll { $0 == peerID }
        }
    }
    
}

Parameters like foundPeer notify when an advertised peer on the same service is found, as well as lostPeer when that peer is no longer available.

Starting and stopping

You can control when the peer advertises or hides itself, or searches for other available devices, by calling:

advertiser.startAdvertisingPeer()
advertiser.stopAdvertisingPeer()

browser.startBrowsingForPeers()
browser.stopBrowsingForPeers()

Here is the complete code, put all together so that you can try it out in your own applications.

import Foundation
import MultipeerConnectivity

class PeerSessionManager: NSObject, ObservableObject {
    
    private let serviceID = "p2p-demo"
    private let peerID = MCPeerID(displayName: UIDevice.current.name)
    
    private var session: MCSession
    private var advertiser: MCNearbyServiceAdvertiser
    private var browser: MCNearbyServiceBrowser
    
    @Published var discoveredPeers: [MCPeerID] = []
    @Published var connectedPeers: [MCPeerID] = []
    
    override init() {
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceID)
        browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceID)
        
        super.init()
        
        session.delegate = self
        advertiser.delegate = self
        browser.delegate = self
    }
    
    func startAdvertising() {
        advertiser.startAdvertisingPeer()
    }
    func stopAdvertising() {
        advertiser.stopAdvertisingPeer()
    }
    func startBrowsing() {
        browser.startBrowsingForPeers()
    }
    func stopBrowsing() {
        browser.stopBrowsingForPeers()
    }
    
}

extension PeerSessionManager: MCSessionDelegate {

    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        DispatchQueue.main.async {
            self.connectedPeers = session.connectedPeers
        }
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        // No implementation for this example
    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        // No implementation for this example
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        // No implementation for this example
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        // No implementation for this example
    }
}

extension PeerSessionManager: MCNearbyServiceAdvertiserDelegate {

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
    }
    
}

extension PeerSessionManager: MCNearbyServiceBrowserDelegate {

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        DispatchQueue.main.async {
            if !self.discoveredPeers.contains(peerID) {
                self.discoveredPeers.append(peerID)
            }
        }
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        DispatchQueue.main.async {
            self.discoveredPeers.removeAll { $0 == peerID }
        }
    }
    
}