visit
iPhone and iPad use Wi-Fi and Bluetooth technology, whereas MacBook and Apple TV rely on Wi-Fi and Ethernet.
Integration of Multipeer Connectivity is not complicated and consists of the following steps:
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____.
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