Getting Started with Multipeer Connectivity

In this tutorial, you’ll learn how to transfer data between devices with no external network. You’ll also try your hand at creating a chat feature. By Andy Pereira.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Sending Data Between Devices

To start sending jobs between devices, first add the following method to JobConnectionManager.swift:

private func send(_ job: JobModel, to peer: MCPeerID) {
  do {
    let data = try JSONEncoder().encode(job)
    try session.send(data, toPeers: [peer], with: .reliable)
  } catch {
    print(error.localizedDescription)
  }
}

Here, you’re encoding the job you want to send. Then, you’re using MCSession‘s send(_:toPeers:with:). This method takes three items:

  1. A Data object you want to send. In this case, it’s the serialized job.
  2. An array of peers you want to send the data to. Since you’re only sending the job to a selected device, only one peer is in the array.
  3. A mode in which to send the data. The use of reliable here isn’t necessary, as you’re only sending one job at a time. This becomes more important if you’re trying to send multiple items and want to guarantee order.

Next, add the following block of code to the end of JobConnectionManager.swift:

extension JobConnectionManager: MCSessionDelegate {
  func session(
    _ session: MCSession,
    peer peerID: MCPeerID,
    didChange state: MCSessionState
  ) {
    switch state {
    case .connected:
      print("Connected")
    case .notConnected:
      print("Not connected: \(peerID.displayName)")
    case .connecting:
      print("Connecting to: \(peerID.displayName)")
    @unknown default:
      print("Unknown state: \(state)")
    }
  }

  func session(
    _ session: MCSession,
    didReceive data: Data,
    fromPeer peerID: MCPeerID
  ) {
  }

  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?
  ) {}
}

There seems to be a lot going on here, but it’s actually quite simple. To send and receive data, you need to conform to MCSessionDelegate. These methods are all required, but this part of the tutorial will only focus on two of them.

Look at session(_:peer:didChange:). This method is called whenever the connection state to another peer changes. When sending a job, one device asks to connect to another. If it grants permission and makes a connection, this method will be called with the state being connected.

In session(_:peer:didChange:), replace the following print statement:

case .connected:
  print("Connected")

In its place, use:

case .connected:
  guard let jobToSend = jobToSend else { return }
  send(jobToSend, to: peerID)

Now, once the devices establish a connection, the device attempting to send the job will call send(_:to:), which you added above.

To handle receiving the job, in JobConnectionManager.swift, replace session(_:didReceive:fromPeer:) with the following:

func session(
  _ session: MCSession,
  didReceive data: Data,
  fromPeer peerID: MCPeerID
) {
  guard let job = try? JSONDecoder()
    .decode(JobModel.self, from: data) else { return }
  DispatchQueue.main.async {
    self.jobReceivedHandler?(job)
  }
}

When a job is sent to an advertising device, it passes the data to this method. Here, decode the data into a JobModel. jobReceivedHandler is called on the main thread, as it’ll need to update the UI.

Now add the following to the end of the initializer for JobConnectionManager:

session.delegate = self

This final step ensures that connecting to a device will call your code to send a job. It also handles decoding and adding a job to the advertising device.

Build and run. Follow the same steps of adding a job on one device and turning on RECEIVE JOBS on the other. On the first device, select the peer found in the job view. On the second device, accept the invitation to receive a job. You’ll now see the same job on both devices:

A job sent from one device to another

Browsing for Sessions

The last part of this tutorial will focus on a few different items as you build out the chat functionality of the app. If you open ChatConnectionManger.swift, you’ll see there’s a lot of code that looks similar to the code in JobConnectionManager.swift.

Build and run, and select the Messages tab. You’ll see the following:

Initial run of showing Messages tab of app

The state of Messages starts where you left off from the first part of this tutorial. If you select Host a Chat Session, it’ll start advertising your device using MCNearbyServiceAdvertiser. However, Messages will use a different service type: jobmanager-chat.

Since hosting a session is complete, you’ll need a way to join a session. When working with jobs, you wrote custom code to find sessions using MCNearbyServiceBrowser. For this part, you’ll use Apple’s default UI for browsing for sessions.

First, in ChatConnectionManager.swift, replace join() with the following:

func join() {
  // 1
  peers.removeAll()
  messages.removeAll()
  session = MCSession(
    peer: myPeerId,
    securityIdentity: nil,
    encryptionPreference: .required)
  session?.delegate = self
  guard 
    let window = UIApplication.shared.windows.first,
    let session = session 
  else { return }
  // 2
  let mcBrowserViewController = MCBrowserViewController(
    serviceType: ChatConnectionManager.service,
    session: session)
  window.rootViewController?.present(mcBrowserViewController, animated: true)
}

Here’s what the code above does:

  1. This first part cleans up any messages or peers and creates a new session for you.
  2. By using MCBrowserViewController, you access a built-in UI for displaying peers that are advertising a service.

Next, add the following to the end of ChatConnectionManager.swift:

extension ChatConnectionManager: MCBrowserViewControllerDelegate {
  func browserViewControllerDidFinish(
    _ browserViewController: MCBrowserViewController
  ) {
    browserViewController.dismiss(animated: true) {
      self.connectedToChat = true
    }
  }

  func browserViewControllerWasCancelled(
    _ browserViewController: MCBrowserViewController
  ) {
    session?.disconnect()
    browserViewController.dismiss(animated: true)
  }
}

This code will handle the two scenarios that can occur when the MCBrowserViewController is presented:

  1. A user has selected a session to join.
  2. A user has canceled browsing.

Next, add the following line of code to the end of join():

mcBrowserViewController.delegate = self

This will ensure your code from the previous step executes.

Finally, in ChatConnectionManager.swift, replace advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:) with the code below:

func advertiser(
  _ advertiser: MCNearbyServiceAdvertiser,
  didReceiveInvitationFromPeer peerID: MCPeerID,
  withContext context: Data?,
  invitationHandler: @escaping (Bool, MCSession?) -> Void
) {
  invitationHandler(true, session)
}

When dealing with sending jobs, you used this method to present an alert to ask the user for permission to accept a job. Here, you’re allowing a connection to the hosting session by default.

Build and run on both devices, and go to Messages. From one device, host a session. On the second device, select Join a Chat Session. You’ll see the following:

The MCBrowserViewController in action

You can select a peer in the browser view controller. Once you see Connected, select Done. You’ll then see the following:

Joined a session using MCBrowserViewController