paint-brush
Master iOS Multipeer Connectivity and Share Data Across Multiple Devices without Internet Accessby@bugorbn
235 reads

Master iOS Multipeer Connectivity and Share Data Across Multiple Devices without Internet Access

by Boris BugorAugust 19th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Multipeer Connectivity allows for direct data exchange between Apple devices using Wi-Fi, Bluetooth, and Ethernet, bypassing traditional servers. The article outlines the benefits, limitations, and steps to integrate this technology into your projects.
featured image - Master iOS Multipeer Connectivity and Share Data Across Multiple Devices without Internet Access
Boris Bugor HackerNoon profile picture


Multipeer Connectivity is an alternative to the common data exchange format. Instead of exchanging data via Wi-Fi or Cellular Network through an intermediate broker, which is usually a backend server, Multipeer Connectivity provides the ability to exchange information between multiple nearby devices without intermediaries.


iPhone and iPad use Wi-Fi and Bluetooth technology, whereas MacBook and Apple TV rely on Wi-Fi and Ethernet.


From here, the pros and cons of this technology immediately follow. The advantages include decentralization and, accordingly, the ability to exchange information without intermediaries.


Disadvantages — sharing is limited to Wi-Fi and Bluetooth coverage for iPhone and iPad, or Wi-Fi and Ethernet for MacBook and Apple TV. In other words, the exchange of information can be carried out in the immediate vicinity of the devices.


Integration of Multipeer Connectivity is not complicated and consists of the following steps:

  1. Project preset

  2. Setup visibility for other devices

  3. Scan for visible devices in range

  4. Creating a pair of devices for data exchange

  5. Data exchange


Let’s take a closer look at each of the above steps.


1. Project preset

At this stage, the project must be prepared for the implementation of Multipeer Connectivity. To do this, you need to obtain additional permissions from the user to be able to scan:

  • add Privacy — Local Network Usage Description to the Info.plist file with a description of the purpose of use;
  • In addition, for the possibility of information exchange, Info.plist also needs to be supplemented with the following lines:


<key>NSBonjourServices</key>
 <array>
  <string>_nearby-devices._tcp</string>
  <string>_nearby-devices._upd</string>
 </array>


It is important to note that the nearby-devices substring is used as an example in this context. In your project, this key must meet the following requirements:

1–15 characters long and valid characters include ASCII lowercase letters, numbers, and the hyphen, containing at least one letter and no adjacent hyphens.

You can read more about the requirements__here__.

As for communication protocols, the example uses tcp and upd (more reliable and less reliable one). If you do not know which protocol you need, you should enter both.

2. Setup visibility for other devices

Organization of device visibility for multi-peer connection is implemented by MCNearbyServiceAdvertiser. Let’s create a class that will be responsible for detecting, displaying, and sharing information between devices.


import MultipeerConnectivity
import SwiftUI

class DeviceFinderViewModel: ObservableObject {
    private let advertiser: MCNearbyServiceAdvertiser
    private let session: MCSession
    private let serviceType = "nearby-devices"

    @Published var isAdvertised: Bool = false {
        didSet {
            isAdvertised ? advertiser.startAdvertisingPeer() : advertiser.stopAdvertisingPeer()
        }
    }
    
    init() {
        let peer = MCPeerID(displayName: UIDevice.current.name)
        session = MCSession(peer: peer)
        
        advertiser = MCNearbyServiceAdvertiser(
            peer: peer,
            discoveryInfo: nil,
            serviceType: serviceType
        )
    }
}


The core of the multipeer is a MCSession, which will allow you to connect and exchange data between devices.

The serviceType is the key mentioned above, which was added to the Info.plist file along with the exchange protocols.

The isAdvertised property will allow you to switch the visibility of the device using Toggle.

3. Scan for visible devices in range

Device visibility scanning for a multi-peer connection is performed by MCNearbyServiceBrowser:


class DeviceFinderViewModel: NSObject, ObservableObject {
    ...
    private let browser: MCNearbyServiceBrowser
    ...

    @Published var peers: [PeerDevice] = []
    ...
    
    override init() {
        ...
        
        browser = MCNearbyServiceBrowser(peer: peer, serviceType: serviceType)
        
        super.init()
        
        browser.delegate = self
    }

    func startBrowsing() {
        browser.startBrowsingForPeers()
    }
    
    func finishBrowsing() {
        browser.stopBrowsingForPeers()
    }
}

extension DeviceFinderViewModel: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        peers.append(PeerDevice(peerId: peerID))
    }

    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        peers.removeAll(where: { $0.peerId == peerID })
    }
}  

struct PeerDevice: Identifiable, Hashable {
    let id = UUID()
    let peerId: MCPeerID
}


A list of all visible devices will be stored in peers. TheMCNearbyServiceBrowserdelegate methods will add or remove an MCPeerID when a peer is found or lost.


ThestartBrowsing and finishBrowsing methods will be used to start discovering visible devices when the screen appears, or stop searching after the screen disappears.


The following View will be used as the UI:


struct ContentView: View {
    @StateObject var model = DeviceFinderViewModel()

    var body: some View {
        NavigationStack {
            List(model.peers) { peer in
                HStack {
                    Image(systemName: "iphone.gen1")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)

                    Text(peer.peerId.displayName)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(.vertical, 5)
            }
            .onAppear {
                model.startBrowsing()
            }
            .onDisappear {
                model.finishBrowsing()
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Toggle("Press to be discoverable", isOn: $model.isAdvertised)
                        .toggleStyle(.switch)
                }
            }
        }
    }
}


Device visibility will be enabled/disabled by the Toggle.
As a result, at this stage, the detection and display of devices should work correctly.


4. Creating a pair of devices for data exchange

The delegate method MCNearbyServiceAdvertiserdidReceiveInvitationFromPeer is responsible for sending an invitation between a pair of devices. Both of them must be capable of handling this request.


class DeviceFinderViewModel: NSObject, ObservableObject {
    ...

    @Published var permissionRequest: PermitionRequest?
    
    @Published var selectedPeer: PeerDevice? {
        didSet {
            connect()
        }
    }

    ...
   
    @Published var joinedPeer: [PeerDevice] = []
    
    override init() {
        ...
               
        advertiser.delegate = self
    }

    func startBrowsing() {
        browser.startBrowsingForPeers()
    }
    
    func finishBrowsing() {
        browser.stopBrowsingForPeers()
    }
    
    func show(peerId: MCPeerID) {
        guard let first = peers.first(where: { $0.peerId == peerId }) else {
            return
        }
        
        joinedPeer.append(first)
    }
    
    private func connect() {
        guard let selectedPeer else {
            return
        }
        
        if session.connectedPeers.contains(selectedPeer.peerId) {
            joinedPeer.append(selectedPeer)
        } else {
            browser.invitePeer(selectedPeer.peerId, to: session, withContext: nil, timeout: 60)
        }
    }
}

extension DeviceFinderViewModel: MCNearbyServiceAdvertiserDelegate {
    func advertiser(
        _ advertiser: MCNearbyServiceAdvertiser,
        didReceiveInvitationFromPeer peerID: MCPeerID,
        withContext context: Data?,
        invitationHandler: @escaping (Bool, MCSession?) -> Void
    ) {
        permissionRequest = PermitionRequest(
            peerId: peerID,
            onRequest: { [weak self] permission in
                invitationHandler(permission, permission ? self?.session : nil)
            }
        )
    }
}

struct PermitionRequest: Identifiable {
    let id = UUID()
    let peerId: MCPeerID
    let onRequest: (Bool) -> Void
}


When the selectedPeer is set, the connect method fires. If this peer is in the list of existing peers, it will be added to the joinedPeer array. In the future, this property will be processed by the UI.


In the absence of this peer in the session, the browser will invite this device to create a pair.


After that, the didReceiveInvitationFromPeer method will be processed for the invited device. In our case, after the start of didReceiveInvitationFromPeer, a permissionRequest is created with a delayed callback, which will be shown as an alert on the invited device:


struct ContentView: View {
    @StateObject var model = DeviceFinderViewModel()

    var body: some View {
        NavigationStack {
            ...
            .alert(item: $model.permissionRequest, content: { request in
                Alert(
                    title: Text("Do you want to join \(request.peerId.displayName)"),
                    primaryButton: .default(Text("Yes"), action: {
                        request.onRequest(true)
                        model.show(peerId: request.peerId)
                    }),
                    secondaryButton: .cancel(Text("No"), action: {
                        request.onRequest(false)
                    })
                )
            })
            ...
        }
    }
}


In the case of an approve, didReceiveInvitationFromPeer will return the device sending the invitation, permission and session if permission was succeed.


As a result, after successfully accepting the invitation, a pair will be created:


5. Data exchange

After creating a pair, MCSession is responsible for the exchange of data:


import MultipeerConnectivity
import Combine

class DeviceFinderViewModel: NSObject, ObservableObject {
 ...

    @Published var messages: [String] = []
    let messagePublisher = PassthroughSubject<String, Never>()
    var subscriptions = Set<AnyCancellable>()

    func send(string: String) {
        guard let data = string.data(using: .utf8) else {
            return
        }
        
        try? session.send(data, toPeers: [joinedPeer.last!.peerId], with: .reliable)

        messagePublisher.send(string)
    }
    
    override init() {

        
        ...
        
        session.delegate = self
        
        messagePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.messages.append($0)
            }
            .store(in: &subscriptions)
    }    
}


extension DeviceFinderViewModel: MCSessionDelegate {
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        guard let last = joinedPeer.last, last.peerId == peerID, let message = String(data: data, encoding: .utf8) else {
            return
        }

        messagePublisher.send(message)
    }
}


Method func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws helps send data between peers.


The delegate method func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) is triggered on the device that received the message.


Also, an intermediate publisher messagePublisher is used to receive messages, since the MCSession delegate methods fire in the DispatchQueue global().


More details on the Multipeer Connectivity integration prototype can be found in this repository. As an example, this technology has provided the ability to exchange messages between devices.



Don’t hesitate to contact me on Twitter if you have any questions. Also, you can always buy me a coffee.