There are times when we may want to share data across instances of our app running on different physical devices. You could develop a server or even leverage cloud storage, but did you know Apple provides a framework to share data directly across devices, without having to use an intermediary? This framework is the MultipeerConnectivity framework, and it has actually been around for a while. In this article, we will explore this framework to understand how we can use it to share data across instances of our app in different devices directly.
The MultipeerConnectivity Framework.
This framework is actually very old. It was introduced in iOS 7 all the way back in 2013. macOS later supported in OS X 10.10 Yosemite in 2014, and it’s even supported by tvOS starting on tvOS 10.0. It supports a wide arrange of devices.
The framework allows you to send basically any kind of data, whether it is short strings of text or images.
How It Works
Before we dive deep into the code, we need to understand, superficially, the technologies it uses under the hood. This is important because you will be able to understand the capabilities and limitations of the framework in case you ever come across code that you expect to work, but doesn’t.
What’s important to know is that the framework can use different mediums to share data and not all devices support the same mediums.
iOS Support
In iOS, the framework can use the following as the underlying medium for data sharing:
- Infrastructure Wi-Fi (AKA the Wi-Fi you have in your house).
- Peer-to-peer Wi-Fi.
- Bluetooth Personal Area Network (PANs).
macOS and tvOS
tvOS and macOS both support the same transport mechanisms:
- Infrastructure Wi-Fi
- Peer-to-peer Wi-Fi
- Ethernet
Overall Architecture
Devices cannot connect and send data to any device willy-nilly. Before two devices can share data, they need to establish a Session (a MCSession
object) with each other.
To do this, one of the device becomes the advertiser and it starts broadcasting to nearby devices. It simply tells them “hey all, I am willing to connect to one, as long as you guys are offering a session of this type. This is done with the MCNearbyServiceAdvertiser
, object, or with the MCAdvertiserAssistant
object. The only difference between these two objects is that the latter provides an UI to accept invitations. If you want to create your own UI to let your user manage their invitations, you can use the former.
Other apps can start looking for advertisers using the MCNearbyServiceBrowser
or MCBrowserViewController
objects. These two objects will let you see which devices are advertising the service type you want to connect to. Just like advertiser objects, the latter provides you with a standard UI, but you can build your own UI with the former if you want.
Finally, all apps running an instance of the app have a MCPeerID
associated to them. This ID is unique to each device.
Playing with the MultipeerConnectivity Framework
With all that theory out of the way, it’s time to write a bit of code. We will explore a few more concepts as we do, so you can understand better how to use this framework.
If you want to use the code here, you may want to get two devices. I will provide a sample project at the end that you can install on two devices so you can see them share data with each other.
Becoming the Advertiser
We will explore how to become an advertiser using MCNearbyServiceAdvertiser
first, as this gives us more control over the UI and experience when establishing a session.
Establishing an MCSession
is a two-step progress. The first step is the discovery step. In the discovery step, a device can start looking for devices to connect to (advertisers who have a MCNearbyNearbyAdvertiser
or MCAdvertiserAssistant
currently advertising) using the MCNearbyServiceBrowser
object.
Advertisers can start a session with code like this:
var advertiser: MCNearbyServiceAdvertiser?
let serviceType = "MPCTutorial"
var myId = MCPeerID(displayName: UIDevice.current.name)
//...
func becomeAdvertiser() {
let discoveryInfo = [
"Device Type": UIDevice.current.model,
"OS": UIDevice.current.systemName,
"OS Version": UIDevice.current.systemVersion
]
advertiser = MCNearbyServiceAdvertiser(peer: advertiserId, discoveryInfo: discoveryInfo, serviceType: serviceType)
advertiser?.delegate = self
advertiser?.startAdvertisingPeer()
}
There is a bit going on here. First, we create a dictionary called discoveryInfo
. During the discovery step, before the devices have had the opportunity to establish a session, they can broadcast limited activity about themselves using this dictionary. In our case we are offering the device name, OS, and OS Version to be seen by other devices who want to connect to us. In certain scenarios this can help provide more information to the devices to ensure they connect to the right one.
When we create our MCNearbyServiceAdvertiser
object, we need to pass in our MCPeerID
. We create our peer ID also using the device name. The discoveryInfo
is the same dictionary we defined earlier. Finally, the serviceType
can be any string you want, as long as it is a maximum of 15 characters long, ASCII characters only, and/or hyphen.
You should only use the init(displayName)
initializer when creating a peer locally. You can persist MCPeerdID
long term to be used later on.
We assign the delegate to self. This is a MCNearbyServiceAdvertiserDelegate
object that will receive events related to device discovery with devices coming to us. We will implement its only method in a bit.
We call startAdvertisingPeer()
and we are ready to be seen by other devices. There is a matching stopAdvertisingPeer()
we can use when we no longer want to be discoverable too.
Searching for devices to connect to
As part of the discovery step, other devices start looking for advertisers. This can be done as easily as:
var myId = MCPeerID(displayName: UIDevice.current.name)
var browser: MCNearbyServiceBrowser?
var connectedPeer: MCPeerID?
//...
func searchForDevices() {
browser = MCNearbyServiceBrowser(peer: inviteeId, serviceType: serviceType)
browser?.delegate = self
browser?.startBrowsingForPeers()
}
We do not need to provide much info to browsers, as most of the info comes from advertisers. Set the delegate as it will receive all the events related to peer discovery. You can startBrowsingForPeers()
and stopBrowsingForPeers()
as you see fit.
Once you start browsing for peers, the browser will call two delegate methods:
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
print("We found a peer!")
print("ID: \(peerID.displayName)")
print("Device Type: \(info?["Device Type"] ?? "")")
print("Version: \(info?["OS Version"] ?? "")")
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print("We lost peer \(peerID.displayName)")
}
So, one by one, you will receive information about peers that show up and peers that disappear, easy as that.
Inviting devices to connect to us
We are following the manual approach in this article, so we will do the manual connecting process.
Once we find a peer we want to connect to (with browser(foundPeer:peerID:withDiscoveryInfo
), we need to create a MCSession
. We use this object to connect to other peers.
func invitePeerToConnect(peerID: MCPeerID) {
session = MCSession(peer: myId)
session?.delegate = self
self.connectedPeer = peerID
browser?.invitePeer(peerID, to: session!, withContext: nil, timeout: 30)
}
You need to set the session delegate to receive events regarding the session, including when the session state changes and when you receive any data.
A bit of discussion on MCSession
is in order. MCSession
has more than one initializer. The second initializer can be used to create secure and encrypted communication channels between both devices. We will not discuss “secure” MCSession
s in this article, but be aware of the init(peer:securityIdentity:encryptionPreference
initializer, as there may need a case in which you need to verify a peer and/or you’ll have the need to share encrypted information. Encryption handling is very transparent. MCEncryptionPreference is just an enum, and you can use encryption without verifying the peer. in iOS 9 and above, it will require encryption by default.
the context
parameter is an optional data that you can use to pass anything to provide even more context. Do not send any sensitive data with this. The connection has not been established yet, so if you are using encryption, this particular piece of data will not be encrypted.
When you invite an advertiser to connect, the advertiser
delegate will call the advertiser(didReceiveInvitationFromPeer peerID:context:invitationHandler
delegate method.
In this example, we will immediately accept the invitation to connect:
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
print("Invitation to connect from \(peerID.displayName)")
print("Accepting invite")
session = MCSession(peer: advertiserId)
invitationHandler(true, session)
}
Sharing Data - The Session Phase.
After the advertiser has accepted the invite, the session phase will start. When the connection state has changed, the session(peer:didChange)
delegate method of MCSession
gets called. When the state is .connected
, we are ready to send data.
func sendImage(toPeer peer: MCPeerID) {
let bundledImage = Bundle.main.url(forResource: "cucoo", withExtension: "png")!
let imageData = try! Data(contentsOf: bundledImage)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
try! self.session?.send(imageData, toPeers: [peer], with: .reliable)
}
}
On the receiving device, the session(_:didReceive:fromPeer:)
from peer will get called, and you can process the image then.
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
print("Did receive data")
if let imageData = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = imageData
}
}
}
The session can receive other kinds of information as well, and it even supports streaming!
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
}
Sample Project
I have created a sample project of everything here. You can download it from here. You need to install it on two devices to see how it works. The UI simply contains three buttons: One to become an advertiser, one to search for devices, and another to send a default image. When you search for a peer, it will automatically send an invite to the first device it sees. After you tap “Search for Devices”, wait a few seconds and tap “send image” on either device. A glorious image of a cucoo will show up on the destination device.
Conclusion
MultipeerConnectivity provides an easy interface to share data between devices. It will automatically choose the right medium to send data. There’ a few things to keep in mind:
- It currently supports 8 peers connected at the same time.
- When the state changes to
.connected
, the connection only lasts a bit when idle. You should try to send data as soon as the connection is established. - We can get it to work with Bonjour and other APIs if we do manual peer management. That is out of the scope of this article.
Conclusion
Reflection is a very interesting feature that allows to create some sort of meta-programming in Swift. While not applicable to many use cases, it’s important to be aware of its existence.