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:
Project preset
Setup visibility for other devices
Scan for visible devices in range
Creating a pair of devices for data exchange
Data exchange
Let’s take a closer look at each of the above steps.
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:
Info.plist
file with a description of the purpose of use;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.
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
.
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
. TheMCNearbyServiceBrowser
delegate 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.
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:
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
Don’t hesitate to contact me on