SwiftUI: Peer-to-Peer (P2P) with Multipeer Connectivity Framework
Let’s make a chat app to send messages and images!
SwiftUI: Peer-to-Peer (P2P) with Multipeer Connectivity Framework

The ***Multipeer Connectivity framework*** supports the discovery of services provided by nearby devices and communicating with those services .
Sounds a little similar to transferring data Between Bluetooth Low Energy Devices with ***Core Bluetooth***?
Here are some of the similarities and differences.
Similarities
- Able to advertise our own device
- Able to discover all near by devices
- Able to transfer message-based data
Differences
- Core Bluetooth supports scanning for specific
serviceUUID- Multipeer Connectivity supports Wi-Fi, peer-to-peer Wi-Fi, and Bluetooth for the underlying transport
- We can also send streaming data and resources such as files with Multipeer Connectivity
- With Multipeer Connectivity, we can decide whether if we want to connect to a device or not, whereas with Core Bluetooth, we do not get to control which
[CBCentral](https://developer.apple.com/documentation/corebluetooth/cbcentral) can subscribe to our[CBCharacteristic](https://developer.apple.com/documentation/corebluetooth/cbcharacteristic), a characteristic of a service provided by our device ([CBPeripheral](https://developer.apple.com/documentation/corebluetooth/cbperipheral)).- Core Bluetooth scanning works in the background (with some constraints)
AND!
Multipeer Connectivity is A LOT EASIER to work with! This is just totally my personal opinion though!
Anyway!
If you are interested in finding out more on how we can transfer data with Core Bluetooth, please feel free to take a look at my previous articles: ***Low Energy Bluetooth (Part1: Peripheral Side) and [Part2: Central Side](https://levelup.gitconnected.com/swiftui-low-energy-bluetooth-part2-central-side-1f3148217334)***.
In this article, we will be making a simple chat app with Multipeer Connectivity to check out the basic ideas and the entire flow!
And of course, full code available on GitHub!
Overview
At a SUPER high level, here are the basic steps for discovering nearby devices, connecting to them, and exchanging data.
- Create an
[MCPeerID](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid) representing our own device - Create an
[MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession) to manage connections with peer devices and support communications. - Advertise our own device with an
[MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser) or[MCAdvertiserAssistant](https://developer.apple.com/documentation/multipeerconnectivity/mcadvertiserassistant) - Browse for other peer devices by creating an
[MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser) or[MCBrowserViewController](https://developer.apple.com/documentation/multipeerconnectivity/mcbrowserviewcontroller) - Send invitations to other devices or accept invitations to establish connections
- Send and receive data with
[MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession) and[MCSessionDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate)
We will be taking a more detailed look at what each class actually does! One by one!
Set Up
Before we start coding, two keys we need to add to our Info.plist.
[NSLocalNetworkUsageDescription](https://developer.apple.com/documentation/BundleResources/Information-Property-List/NSLocalNetworkUsageDescription)[NSBonjourServices](https://developer.apple.com/documentation/BundleResources/Information-Property-List/NSBonjourServices)
***Bonjour***?!!
It is a zero-configuration networking provided by Apple that enables automatic discovery of devices and services on a local network using industry standard IP protocols. And it is what Multipeer Connectivity uses to search for nearby devices after iOS 14.
<key>NSLocalNetworkUsageDescription</key>
<string>Needed for multipeer!</string>
<key>NSBonjourServices </key>
<array>
<string>_p2p._tcp</string>
<string>_p2p._udp</string>
</array>

We have two items within the Bonjour services array. The first substring identifies the application protocol and the second identifies the transport protocol.
It also needs to match the serviceType that we will be using later while creating [MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser) and [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser).
Implementation
Like always, we will have an Observable MultipeerManager to help us managing the connection related logic and implementing the delegation methods.
Full Code
import SwiftUI
import MultipeerConnectivity
@Observable
class MultipeerManager: NSObject {
enum _Error: Error {
case invitationFailed(String)
case startBrowsingFailed(String)
case startAdvertisingFailed(String)
case sendMessageFailed(String)
var message: String {
switch self {
case .invitationFailed(let text):
text
case .startBrowsingFailed(let text):
text
case .startAdvertisingFailed(let text):
text
case .sendMessageFailed(let text):
text
}
}
}
struct Message {
var isSent: Bool
var data: Data
}
var error: _Error? = nil {
didSet {
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.error = nil
})
}
}
var isAdvertising: Bool = false {
didSet {
isAdvertising ? startAdvertising() : stopAdvertising()
}
}
var isBrowsing: Bool = false {
didSet {
isBrowsing ? startBrowsing() : stopBrowsing()
}
}
// peers that are not connected and are available to invite
var peersAvailableToInvite: [MCPeerID : [String : String]?] {
discoveredPeers.filter({ discoveredPeer in
!managedPeers.contains(where: { managedPeer in
return discoveredPeer.key == managedPeer.key && managedPeer.value.0 == .connected
})
})
}
private var discoveredPeers: [MCPeerID : [String : String]?] = [:]
// invitations from other devices
var invitationsReceived: [MCPeerID : (Data?, (Bool, MCSession?) -> Void)] = [:]
// peers managed by the MCSession
var managedPeers: [MCPeerID : (MCSessionState?, [Message])] = [:]
private let serviceType = "p2p" // same as that in info.plist
private static let peerIdKey = "peerIdKey"
private var peerIDData: Data? = UserDefaults.standard.data(forKey: MultipeerManager.peerIdKey) {
didSet {
UserDefaults.standard.set(peerIDData, forKey: MultipeerManager.peerIdKey)
}
}
private var session: MCSession?
private var advertiser: MCNearbyServiceAdvertiser?
private var browser: MCNearbyServiceBrowser?
override init() {
super.init()
let peerID: MCPeerID
if let peerIDData, let _peerID = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: peerIDData) {
peerID = _peerID
} else {
peerID = MCPeerID(displayName: UIDevice.current.name)
peerIDData = try? NSKeyedArchiver.archivedData(withRootObject: peerID, requiringSecureCoding: true)
}
// session
let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
self.session = session
// advertiser
advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: [
"nickname": "Itsuki"
], serviceType: serviceType)
advertiser?.delegate = self
// browser
browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
browser?.delegate = self
}
// session
func disconnectSession() {
session?.disconnect()
}
func send(_ data: Data, to peerID: MCPeerID) {
do {
try session?.send(data, toPeers: [peerID], with: .reliable)
DispatchQueue.main.async {
self.managedPeers[peerID]?.1.append(Message(isSent: true, data: data))
}
} catch (let error) {
setError(.sendMessageFailed(error.localizedDescription))
}
}
// Advertisement
private func startAdvertising() {
advertiser?.startAdvertisingPeer()
}
private func stopAdvertising() {
advertiser?.stopAdvertisingPeer()
}
func handleInvitation(_ peerID: MCPeerID, accept: Bool) {
guard let info = invitationsReceived[peerID] else {
return
}
info.1(accept, session)
DispatchQueue.main.async {
self.managedPeers[peerID] = (nil as MCSessionState?, [])
self.invitationsReceived.removeValue(forKey: peerID)
}
}
// Browse
private func startBrowsing() {
browser?.startBrowsingForPeers()
}
private func stopBrowsing() {
browser?.stopBrowsingForPeers()
}
func invite(_ peerID: MCPeerID, context: Data? = nil, timeout: TimeInterval /*sec*/) {
guard let session else {
setError(.invitationFailed("Session not available."))
return
}
browser?.invitePeer(peerID, to: session, withContext: context, timeout: timeout)
DispatchQueue.main.async {
self.managedPeers[peerID] = (nil as MCSessionState?, [])
}
}
private func setError(_ error: _Error) {
print("error: \(error)")
DispatchQueue.main.async {
self.error = error
}
}
}
extension MultipeerManager: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
print("found peer: \(peerID.displayName) with info \(String(describing: info))")
DispatchQueue.main.async {
self.discoveredPeers[peerID] = info
}
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print("lost peer: \(peerID.displayName)")
DispatchQueue.main.async {
self.discoveredPeers.removeValue(forKey: peerID)
}
}
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: any Error) {
self.setError(.startBrowsingFailed("Failed to start browsing for peers with error: \(error.localizedDescription)"))
}
}
extension MultipeerManager: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
print("invitation received from \(peerID.displayName), with context: \(String(describing: context?.string))")
DispatchQueue.main.async {
self.invitationsReceived[peerID] = (context, invitationHandler)
}
}
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: any Error) {
self.setError(.startAdvertisingFailed("Failed to start advertising with error: \(error.localizedDescription)"))
}
}
extension MultipeerManager: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
print("peer state changed for \(peerID.displayName): \(state.displayString)")
DispatchQueue.main.async {
self.managedPeers[peerID]?.0 = state
}
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
DispatchQueue.main.async {
self.managedPeers[peerID]?.1.append(Message(isSent: false, data: data))
}
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
print("receive stream.")
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
print("start receiving resource with progress: \(progress)")
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) {
print("finish receiving resource. url: \(String(describing: localURL)), error: \(String(describing: error))")
}
}
extension Data {
var string: String? {
String(data: self, encoding: .utf8)
}
var image: Image? {
if let uiImage = UIImage(data: self) {
return Image(uiImage: uiImage)
} else {
return nil
}
}
}
extension String {
var data: Data? {
self.data(using: .utf8)
}
}
extension MCSessionState {
var displayString: String {
switch self {
case .notConnected:
return "Not Connected"
case .connecting:
return "Connecting..."
case .connected:
return "Connected"
@unknown default:
return "Unknown"
}
}
}
Time to break it down!
MCPeerID
[MCPeerID](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid) is what uniquely identify our app running on a device to nearby peers.
We create a new peer ID by calling [init(displayName:)](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid/init(displayname:)). Here are couple important points to keep in mind.
First of all, the [displayName](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid/displayname) must be no longer than 63 bytes in UTF-8 encoding.
Secondly, each peer ID creates with [init(displayName:)](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid/init(displayname:)) is unique, even when supplying the same display name. That is if we call this function every time our app launches, the [MCPeerID](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid) created will be different every time.
If we want to keep it the same, we can archive it with [NSKeyedArchiver.archivedData](https://developer.apple.com/documentation/foundation/nskeyedarchiver/2962880-archiveddata) and unarchive it with [NSKeyedUnarchiver.unarchivedObject](https://developer.apple.com/documentation/foundation/nskeyedunarchiver/2983380-unarchivedobject). The data will be stored in UserDefaults.
let peerID: MCPeerID
if let peerIDData, let _peerID = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: peerIDData) {
peerID = _peerID
} else {
peerID = MCPeerID(displayName: "\(UIDevice.current.name) by Itsuki")
peerIDData = try? NSKeyedArchiver.archivedData(withRootObject: peerID, requiringSecureCoding: true)
}
MCSession (Part 1)
An [MCSession](https://developer.apple.com/documentation/multipeerconnectivity/mcsession) enables and manages communication among all peers in a Multipeer Connectivity session. To create a session, all we have to do is to call [init(peer:securityIdentity:encryptionPreference:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsession/init(peer:securityidentity:encryptionpreference:)) using the [MCPeerID](https://developer.apple.com/documentation/multipeerconnectivity/mcpeerid) we created/retrieved above.
let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
self.session = session
identity is an array containing information that can be used to identify the local peer to other nearby peers. It is used to sent certificates that nearby peers might require when verifying the local peer’s identity. We have nil here but if you want to find out more on how this parameter is constructed, please check out the ***official document***.
encryptionPreference is an [MCEncryptionPreference](https://developer.apple.com/documentation/multipeerconnectivity/mcencryptionpreference) that indicates whether the connection prefers encrypted connections, unencrypted connections, or has no preference.
[optional](https://developer.apple.com/documentation/multipeerconnectivity/mcencryptionpreference/optional) if the session prefers to use encryption, but accepts unencrypted connections.[required](https://developer.apple.com/documentation/multipeerconnectivity/mcencryptionpreference/required) to require encryption.[none](https://developer.apple.com/documentation/multipeerconnectivity/mcencryptionpreference/none) for unencrypted connections
We will be taking a look at using MCSession to send/receive data and manage connections after checking out advertiser and browser.
Advertiser
To advertise our device so that nearby peers can find us, we can either use an [MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser) or an [MCAdvertiserAssistant](https://developer.apple.com/documentation/multipeerconnectivity/mcadvertiserassistant).
MCAdvertiserAssistant provides the same functionality as the advertiser with a standard user interface that allows the user to accept invitations. I want a little more control over the entire process so I will be using the advertiser object here.
To initialize an advertiser, we have the [init(peer:discoveryInfo:serviceType:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser/init(peer:discoveryinfo:servicetype:)) function.
advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: [
"nickname": "Itsuki"
], serviceType: serviceType)
advertiser?.delegate = self
myPeerID is the MCPeerID we have created in the beginning.
info is a a dictionary of key-value pairs that are made available to browsers with couple constraints.
- Each key and value must be an
NSStringobject. - The key-value pair must be no longer than 255 bytes (total) when encoded in UTF-8 format with an equals sign (
=) between the key and the value. - Keys cannot contain an equals sign
- The total size of the keys and values in this dictionary must be no longer than 65,535 bytes. Also, it is strongly recommended to keep it within 400 bytes.
serviceType specifies the type of service to advertise.
It should be in the same format as a Bonjour service type we added to our Info.plist without the transport protocol. In my example, that would be p2p.
The string needs to meet the restrictions of RFC 6335 (section 5.1) governing Service Name Syntax. By that means, it
- must be 1–15 characters long
- can contain only ASCII lowercase letters, numbers, and hyphens
- must contain at least one ASCII letter
- must not begin or end with a hyphen
- must not contain hyphens adjacent to other hyphens
To start and stop advertising, we can simply call [startAdvertisingPeer()](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser/startadvertisingpeer()) and [stopAdvertisingPeer()](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser/stopadvertisingpeer()) on the [MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser) object.
private func startAdvertising() {
advertiser?.startAdvertisingPeer()
}
private func stopAdvertising() {
advertiser?.stopAdvertisingPeer()
}
Advertiser Delegate
To find out whether if an advertisement failed to start or to be notified when we receive an invitation from nearby peers, we have the [MCNearbyServiceAdvertiserDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiserdelegate) protocol.
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
print("invitation received from \(peerID.displayName), with context: \(String(describing: context?.string))")
DispatchQueue.main.async {
self.invitationsReceived[peerID] = (context, invitationHandler)
}
}
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: any Error) {
self.setError(.startAdvertisingFailed("Failed to start advertising with error: \(error.localizedDescription)"))
}
First of all, we have [advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiserdelegate/advertiser(_:didreceiveinvitationfrompeer:withcontext:invitationhandler:)). This is the only method required for conforming to the protocol. It is called when an invitation to join a session is received from a nearby peer with peerID.
context will contain an arbitrary piece of data received from the nearby peer. This will be the data set by [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser) when inviting. We will be taking a look at this shortly.
invitationHandler is a block that our code must call to indicate whether we will accept or decline the invitation. If we have decided to accept the invitation, we will also be using this block to provide a session with which to associate the peer that sent the invitation.
func handleInvitation(_ peerID: MCPeerID, accept: Bool) {
guard let info = invitationsReceived[peerID] else {
return
}
info.1(accept, session)
DispatchQueue.main.async {
self.managedPeers[peerID] = (nil as MCSessionState?, [])
self.invitationsReceived.removeValue(forKey: peerID)
}
}
Note that if we don’t call the block within the [timeout](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser/invitepeer(_:to:withcontext:timeout:)) time specified by the browser, nothing will happen on the advertising side. We will be taking a look at what will happen on the browser side in couple seconds.
(We actually don’t even have a way to find out the timeout time on the advertising side…At least not that I know of…)
We then have [advertiser(_:didNotStartAdvertisingPeer:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiserdelegate/advertiser(_:didnotstartadvertisingpeer:)). This method is called when an advertisement failed and is not required.
Browser
Similar to advertiser, we have [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser) for searching for nearby devices and an [MCBrowserViewController](https://developer.apple.com/documentation/multipeerconnectivity/mcbrowserviewcontroller) that basically provides the same functionality plus a standard user interface.
The second we see the word ViewController, we should try to ditch it if we are using SwiftUI.
— by Itsuki
That is we are using [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser) here.
To create an MCNearbyServiceBrowser object, we have [init(peer:serviceType:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser/init(peer:servicetype:)).
browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
browser?.delegate = self
We, again, have myPeerID representing our own device and serviceType. However, this time, the serviceType is the type of service to search for and it should be what describes the app’s networking protocol (not transport protocol). In my example, that’s p2p as well.
We can then start and stop browsing using [startBrowsingForPeers()](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser/startbrowsingforpeers()) and [stopBrowsingForPeers()](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser/stopbrowsingforpeers()) respectively.
private func startBrowsing() {
browser?.startBrowsingForPeers()
}
private func stopBrowsing() {
browser?.stopBrowsingForPeers()
}
Browser Delegate
[MCNearbyServiceBrowserDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowserdelegate) defines methods for handling browser-related events.
Specifically, we have [browser(_:foundPeer:withDiscoveryInfo:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowserdelegate/browser(_:foundpeer:withdiscoveryinfo:)) for notifications on peer found, [browser(_:lostPeer:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowserdelegate/browser(_:lostpeer:)) for peer lost, and [browser(_:didNotStartBrowsingForPeers:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowserdelegate/browser(_:didnotstartbrowsingforpeers:)) when browser failed to start browsing.
extension MultipeerManager: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
print("found peer: \(peerID.displayName) with info \(String(describing: info))")
DispatchQueue.main.async {
self.discoveredPeers[peerID] = info
}
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print("lost peer: \(peerID.displayName)")
DispatchQueue.main.async {
self.discoveredPeers.removeValue(forKey: peerID)
}
}
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: any Error) {
self.setError(.startBrowsingFailed("Failed to start browsing for peers with error: \(error.localizedDescription)"))
}
}
After we found our nearby peers, we can then send them invitations to join a specific MCSession with [invitePeer(_:to:withContext:timeout:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser/invitepeer(_:to:withcontext:timeout:)).
session will be The session that we wish the invited peer to join.
context is the data delivered to the [advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:)](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiserdelegate/advertiser(_:didreceiveinvitationfrompeer:withcontext:invitationhandler:)) delegation method above for [MCNearbyServiceAdvertiserDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiserdelegate). This can be used to provide further information about the invitation.
timeout is the amount of time in seconds to wait for the peer to respond to the invitation. If a negative value or zero is specified, the default timeout (30 seconds) is used.
What if the invited peer does not respond within the timeout time?
We will be notified (some how?) within the [session(_:peer:didChange:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:peer:didchange:)) method of [MCSessionDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate) that we will be taking a look next.
MCSession (Part 2) & Session Delegate
[MCSessionDelegate](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate) defines a set of methods to handle session-related events.
extension MultipeerManager: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
print("peer state changed for \(peerID.displayName): \(state.displayString)")
DispatchQueue.main.async {
self.managedPeers[peerID]?.0 = state
}
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
DispatchQueue.main.async {
self.managedPeers[peerID]?.1.append(Message(isSent: false, data: data))
}
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
print("receive stream.")
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
print("start receiving resource with progress: \(progress)")
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) {
print("finish receiving resource. url: \(String(describing: localURL)), error: \(String(describing: error))")
}
}
(Since I am not planning on supporting sending stream or resources, I am simply printing out on the corresponding delegation methods.)
First of all, we have [session(_:peer:didChange:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:peer:didchange:)) that is called when the state of a nearby peer changes to
[MCSessionState.connected](https://developer.apple.com/documentation/multipeerconnectivity/mcsessionstate/connected) representing the nearby peer accepted the invitation and is now connected to the session.[MCSessionState.notConnected](https://developer.apple.com/documentation/multipeerconnectivity/mcsessionstate/notconnected) if the nearby peer declined the invitation, the connection could not be established, or a previously connected peer is no longer connected.
(That’s why I said some how above. We will not actually know whether if the notConnected state is due to timeout, decline, or just a connection lost from the method itself. You could keep a timer by yourself to check though…)
Assuming the peer you are inviting is a super nice person and decide to accept your invitation, we can now start sending them stuff(?!) with
[send(_:toPeers:with:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsession/send(_:topeers:with:)) for message-based data. Contents send with this method will trigger[session(_:didReceive:fromPeer:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:didreceive:frompeer:)) method on the recipient device, after it has been fully received.[sendResource(at:withName:toPeer:withCompletionHandler:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsession/sendresource(at:withname:topeer:withcompletionhandler:)) to send the contents of a URL. This can be either a file URL or an HTTP URL. On the local device, the completion handler block is called when delivery succeeds or when an error occurs. On the recipient device,[session(_:didStartReceivingResourceWithName:fromPeer:with:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:didstartreceivingresourcewithname:frompeer:with:)) will be called as soon as it begins receiving the resource. And upon successful delivery,[session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:didfinishreceivingresourcewithname:frompeer:at:witherror:))will be invoked with the resource written to a file in a temporary location with the same base name. We will need to either open the file or move it to a permanent location before the delegate method returns.[startStream(withName:toPeer:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsession/startstream(withname:topeer:)) to open a byte stream.[session(_:didReceive:withName:fromPeer:)](https://developer.apple.com/documentation/multipeerconnectivity/mcsessiondelegate/session(_:didreceive:withname:frompeer:)) will be called on the recipient device. For more information about performing networking with input and output streams, please refer to ***Networking Programming Topics***.
All three methods above are asynchronous (nonblocking).
Lastly, we have [disconnect()](https://developer.apple.com/documentation/multipeerconnectivity/mcsession/disconnect()) to disconnect all local peers from the session. When our app goes into background, Multipeer framework will actually automatically disconnects any open sessions, but just in case, you can also call this function for manual disconnection.
Usage
Let’s finish our day with couple simple views so that we can use what we have above to chat with our nearby peers!
import SwiftUI
import MultipeerConnectivity
import PhotosUI
struct ContentView: View {
@State private var multipeerManager = MultipeerManager()
@Environment(\.scenePhase) private var scenePhase
@State private var showInvitationSheet = false
@State private var selectedPeerID: MCPeerID? = nil
@State private var invitationMessage: String = ""
@State private var invitationTimeout: Int = 30
var body: some View {
@Bindable var multipeerManager = multipeerManager
List {
if let error = multipeerManager.error {
Text(error.message)
.multilineTextAlignment(.leading)
.foregroundStyle(.red)
}
Section {
if multipeerManager.peersAvailableToInvite.isEmpty {
Text("No peer available to invite.")
.foregroundStyle(.secondary)
}
ForEach(Array(multipeerManager.peersAvailableToInvite.keys), id: \.self) { key in
let peerID: MCPeerID = key
let discoveryInfo = multipeerManager.peersAvailableToInvite[key] ?? [:]
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text(peerID.displayName)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.8)
if let discoveryInfo = discoveryInfo, discoveryInfo.isEmpty == false {
VStack(alignment: .leading, spacing: 0) {
Text("additional Info:")
ForEach(Array(discoveryInfo.keys), id: \.self) { discoveryInfoKey in
if let info = discoveryInfo[discoveryInfoKey] {
Text("\(discoveryInfoKey): \(info)")
}
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
} else {
Text("No additional Info available.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
Spacer()
Button(action: {
selectedPeerID = peerID
showInvitationSheet = true
}, label: {
Text("Invite")
})
.buttonStyle(.borderless)
.font(.headline)
}
}
} header: {
HStack {
Text("Browse")
Toggle("", isOn: $multipeerManager.isBrowsing)
.scaleEffect(0.7, anchor: .trailing)
}
}
Section {
if multipeerManager.invitationsReceived.isEmpty {
Text("No invitation received.")
.foregroundStyle(.secondary)
}
ForEach(Array(multipeerManager.invitationsReceived.keys), id: \.self) { key in
let peerID: MCPeerID = key
let context: Data? = multipeerManager.invitationsReceived[key]?.0
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 4) {
Text(peerID.displayName)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.8)
Text("Message: \(context?.string ?? "(no message)")")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxHeight: .infinity)
Spacer()
VStack(alignment: .trailing, spacing: 4) {
Button(action: {
multipeerManager.handleInvitation(peerID, accept: true)
}, label: {
Text("Accept")
})
Button(action: {
multipeerManager.handleInvitation(peerID, accept: false)
}, label: {
Text("Decline")
})
}
.buttonStyle(.borderless)
.font(.subheadline)
.frame(maxHeight: .infinity)
}
.fixedSize(horizontal: false, vertical: true)
}
} header: {
VStack(alignment: .leading) {
HStack {
Text("Advertise")
Toggle("", isOn: $multipeerManager.isAdvertising)
.scaleEffect(0.7, anchor: .trailing)
}
Text("Invitation Received")
}
}
Section {
if multipeerManager.managedPeers.isEmpty {
VStack(alignment: .leading) {
Text("No peers added.")
Text("Send or accept an invitation to get started.")
.font(.subheadline)
}
.foregroundStyle(.secondary)
}
ForEach(Array(multipeerManager.managedPeers.keys), id: \.self) { key in
let peerID: MCPeerID = key
let state: MCSessionState? = multipeerManager.managedPeers[key]?.0 ?? nil
NavigationLink(destination: {
PeerInteractionView(peerID: peerID)
.environment(multipeerManager)
}, label: {
HStack {
Text(peerID.displayName)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.8)
Spacer()
if let state {
Text(state.displayString)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
})
}
} header: {
HStack {
Text("Current Session")
}
}
}
.buttonStyle(.plain)
.navigationTitle("Let's Connect!")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: scenePhase, {
print("scene phase changed")
if scenePhase == .background || scenePhase == .inactive {
self.multipeerManager.isBrowsing = false
self.multipeerManager.isAdvertising = false
self.multipeerManager.disconnectSession()
}
})
.sheet(isPresented: $showInvitationSheet, content: {
VStack(spacing: 24) {
HStack {
Text("Invitation Details")
.font(.headline)
.lineLimit(1)
HStack(spacing: 16) {
Button(action: {
showInvitationSheet = false
}, label: {
Text("Cancel")
.foregroundStyle(.red)
})
.buttonStyle(.bordered)
Button(action: {
guard let peerID = selectedPeerID else {
return
}
let timeout = TimeInterval(invitationTimeout)
multipeerManager.invite(peerID, context: invitationMessage.data, timeout: timeout)
showInvitationSheet = false
}, label: {
Text("Send")
})
.buttonStyle(.bordered)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
VStack(alignment: .leading) {
Text("Messages")
TextField("", text: $invitationMessage, axis: .vertical)
.lineLimit(2, reservesSpace: true)
}
VStack(alignment: .leading) {
HStack {
Text("Timeout (sec)")
Spacer()
TextField("", value: $invitationTimeout, format: .number)
.keyboardType(.numberPad)
.frame(width: 48)
}
Text("If a negative value or zero is specified, the default timeout (30 seconds) is used.")
.multilineTextAlignment(.leading)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.textFieldStyle(.roundedBorder)
.padding(.all, 32)
.frame(maxHeight: .infinity, alignment: .top)
.presentationDetents(.init([.height(360)]))
.onAppear {
invitationMessage = ""
}
.onChange(of: showInvitationSheet, initial: true, {
if !showInvitationSheet {
selectedPeerID = nil
}
})
})
}
}
struct TransferableImage: Transferable {
let image: UIImage
enum TransferError: Error {
case importFailed
}
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
guard let uiImage = UIImage(data: data) else {
throw TransferError.importFailed
}
return TransferableImage(image: uiImage)
}
}
}
struct PeerInteractionView: View {
@Environment(MultipeerManager.self) private var multipeerManager
var peerID: MCPeerID
@State private var text: String = ""
@State private var pickerItem: PhotosPickerItem?
@State private var messagingError: String? = nil
var body: some View {
let state: MCSessionState? = multipeerManager.managedPeers[peerID]?.0 ?? nil
let messages: [MultipeerManager.Message] = multipeerManager.managedPeers[peerID]?.1 ?? []
ScrollViewReader { proxy in
List {
if let error = multipeerManager.error {
Text(error.message)
.multilineTextAlignment(.leading)
.foregroundStyle(.red)
}
if let error = messagingError {
Text(error)
.multilineTextAlignment(.leading)
.foregroundStyle(.red)
}
if messages.isEmpty {
VStack(alignment: .leading) {
if state == .connected {
Text("No interactions made yet")
Text("Send a message or an image to get started!")
.font(.subheadline)
} else {
Text("Connect to interact!")
}
}
.foregroundStyle(.secondary)
}
ForEach(0..<messages.count, id: \.self) { index in
let message = messages[index]
HStack(alignment: .top, spacing: 16) {
if !message.isSent {
Text(String(peerID.displayName.first ?? "?"))
.fontWeight(.bold)
.frame(width: 40, height: 40)
.background(Circle().fill(.gray.opacity(0.2)))
}
if let string = message.data.string {
Text(string)
.multilineTextAlignment(.leading)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(RoundedRectangle(cornerRadius: 8).fill(.white))
} else if let image = message.data.image {
image
.resizable()
.scaledToFit()
.frame(maxWidth: UIScreen.main.bounds.width * 0.8, maxHeight: UIScreen.main.bounds.height * 0.4)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(RoundedRectangle(cornerRadius: 8).fill(.white))
} else {
Text("Failed to decode data...")
.multilineTextAlignment(.leading)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(RoundedRectangle(cornerRadius: 8).fill(.white))
}
if message.isSent {
Text("ME")
.fontWeight(.bold)
.frame(width: 40, height: 40)
.background(Circle().fill(.gray.opacity(0.2)))
}
}
.listRowBackground(Color.clear)
.frame(maxWidth: .infinity, alignment: message.isSent ? .trailing : .leading )
.padding(message.isSent ? .leading : .trailing, 32)
}
}
.onChange(of: messages.count, {
proxy.scrollTo(messages.count - 1, anchor: .bottom)
})
}
.listRowSpacing(16)
.listRowSeparator(.hidden)
.navigationTitle("\(peerID.displayName)")
.navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .bottom, content: {
HStack(alignment: .top, spacing: 16) {
TextField("Some message...", text: $text, axis: .vertical)
.lineLimit(5, reservesSpace: false)
.textFieldStyle(.roundedBorder)
HStack(spacing: 8) {
PhotosPicker(selection: $pickerItem, matching: .images, photoLibrary: .shared()) {
Image(systemName: "photo")
.frame(width: 32, height: 32)
}
Button(action: {
if let data = text.data {
multipeerManager.send(data, to: peerID)
text = ""
} else {
self.messagingError = "Failed to convert text to data."
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.messagingError = nil
})
}
}, label: {
Image(systemName: "paperplane.fill")
.frame(width: 32, height: 32)
})
}
}
.padding(.vertical, 16)
.padding(.horizontal, 16)
.frame(maxWidth: .infinity)
.background(.white)
})
.buttonStyle(.plain)
.onChange(of: pickerItem) {
Task {
if let loaded = try? await pickerItem?.loadTransferable(type: TransferableImage.self), let imageData = loaded.image.pngData() {
multipeerManager.send(imageData, to: peerID)
} else {
self.messagingError = "Failed to load image"
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.messagingError = nil
})
}
}
}
}
}

(I probably should have added another UI to allow my user to set the displayName of the MCPeerID so not all of those starts with the letter I for iPhone…)
Oh! And by the way, if you want to find out more about using the [Transferable](https://developer.apple.com/documentation/CoreTransferable/Transferable) protocol or the PhotoPicker, please feel free to check out one of my previous articles, ***SwiftUI: Photos Picker (Image, Movie, Single, Multiple, and Upside Down Pitfall!)***.
Thank you for reading!
That’s it for this article! Hope you enjoyed it! (I definitely did!)
We have used [MCNearbyServiceAdvertiser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyserviceadvertiser) and [MCNearbyServiceBrowser](https://developer.apple.com/documentation/multipeerconnectivity/mcnearbyservicebrowser) for peer discovery here, but you can also ditch both of those and ***managing the connections manually***! By yourself!
See? It is a lot shorter than my super long BLE related ones: ***Low Energy Bluetooth (Part1: Peripheral Side) and [Part2: Central Side](https://levelup.gitconnected.com/swiftui-low-energy-bluetooth-part2-central-side-1f3148217334)***!
So! It has to mean that Multipeer framework is a little easier to work with the Core Bluetooth!
Anyway!
Happy multi-peering!
메타데이터
- post_id
- eb76f13e2b4e
- slug
- swiftui-peer-to-peer-p2p-with-multipeer-connectivity-framework-eb76f13e2b4e
- url
- https://levelup.gitconnected.com/swiftui-peer-to-peer-p2p-with-multipeer-connectivity-framework-eb76f13e2b4e
- canonical_url
- https://levelup.gitconnected.com/swiftui-peer-to-peer-p2p-with-multipeer-connectivity-framework-eb76f13e2b4e
- author_url
- https://medium.com/@itsuki.enjoy
- status
- ok
- fetched_at
- 2026-09-05 13:46:36