Home iOS & Swift Tutorials

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.

5/5 9 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

Most people think of the internet when asked how to transfer data between devices. With today’s modern devices and increasing cellular and Wi-Fi coverage, it seems logical. Yet, there are still cases where the internet may not be available to your users and they still need to transfer data. Think of a time you needed to send a photo to someone you were with, perhaps while traveling abroad. Or maybe you needed to send a file to a coworker in a crowded airport. These are a few connectivity examples users of your app may encounter while in the real world.

Apple’s Multipeer Connectivity framework introduces a way to send and receive data between users of your app. Multipeer Connectivity uses your device’s networking hardware, such as Bluetooth and Wi-Fi, without requiring an internet connection. Think about the last time you used AirDrop to send a photo or web page to someone. Both devices were likely not on the same network, but they were still able to send information from one device to another. Multipeer Connectivity is the same framework that powers AirDrop, and it’s available for you to include in your app.

In this tutorial, you’ll learn how to:

  • Advertise your device to other users of your app.
  • Browse for users that are advertising.
  • Send custom data between devices.
  • Host a session multiple users can join.
  • Send data to multiple users at the same time.
  • Send files stored on your device to another user.
Note: For this tutorial, you’ll need to run the project on two different devices. This can be any combination of physical devices or simulators.

Getting Started

To get started, click the Download Materials button at the top or bottom of this tutorial to download the resources you’ll need. Open the starter project in the starter folder.

The app you’ll use, Job Manager, provides managers and employees with a way to create and assign jobs to each other. It also lets employees host or join chat rooms with other nearby employees.

Build and run, and you’ll see the following:

App running after downloading materials

Now you’ll move on to advertising your device.

Advertising Your Device

To get started, open Info.plist and add the following entry:

  • Key: Privacy — Local Network Usage Description
  • Value: Job Manager needs to use your phone’s data to discover devices nearby

Apple requires this key to inform users why your app will use the local network. In networking, a local network is one in which devices share the same means of communication. When using Multipeer Connectivity, you’re creating a new local network for devices to connect to.

Next, open JobConnectionManager.swift and add the following:

import MultipeerConnectivity

This will ensure you can use the Multipeer Connectivity framework in this file.

Next, add the following to JobConnectionManager.swift inside the class declaration:

// 1
private let session: MCSession
// 2
private let myPeerId = MCPeerID(displayName: UIDevice.current.name)
private let jobReceivedHandler: JobReceivedHandler?

init(_ jobReceivedHandler: JobReceivedHandler? = nil) {
  // 3
  session = MCSession(
    peer: myPeerId,
    securityIdentity: nil,
    encryptionPreference: .none)
  // 4
  self.jobReceivedHandler = jobReceivedHandler
}

Here’s a breakdown of the code above:

  1. MCSession is the class used to handle all communication between devices.
  2. MCPeerID identifies your device on the local network. In this example, you’re using the name you set for your phone.
  3. Initialize your session with your peer ID. You can choose whether you want encryption used for your messages. It’s not used here.
  4. You’ll see more on this last line a little later.

This app allows employees to determine whether they want to allow jobs to be sent to their phone through Advertising. To implement Advertising, add the following properties to JobConnectionManager:

private static let service = "jobmanager-jobs"
private var nearbyServiceAdvertiser: MCNearbyServiceAdvertiser

MCNearbyServiceAdvertiser is the class that will handle making your device discoverable through MCSession. One of the requirements to advertise is that you have a service. The next step will cover this.

Now, add the following to the initializer of JobConnectionManager:

nearbyServiceAdvertiser = MCNearbyServiceAdvertiser(
  peer: myPeerId,
  discoveryInfo: nil,
  serviceType: JobConnectionManager.service)

Here, you initialized nearbyServiceAdvertiser with a Service Type. Multipeer Connectivity uses the service type to limit the way it handles discovering advertised devices. In this project, JobConnectionManager will only be able to discover devices that advertise with the service name of jobmanager-jobs.

Next, add the following to JobConnectionManager.swift, at the end of the file below the class declaration:

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

Once your device is advertising that it’s available for jobs, it’ll need a way to handle requests to connect and receive the job. MCNearbyServiceAdvertiserDelegate needs to handle this request. Here, you can decide whether you want your app to automatically accept the invitation or ask users if they want to allow the connection.

To handle the invitation, add the following to advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:):

guard 
  let window = UIApplication.shared.windows.first,
  // 1
  let context = context,
  let jobName = String(data: context, encoding: .utf8) 
else {
  return
}
let title = "Accept \(peerID.displayName)'s Job"
let message = "Would you like to accept: \(jobName)"
let alertController = UIAlertController(
  title: title,
  message: message,
  preferredStyle: .alert)
alertController.addAction(UIAlertAction(
  title: "No",
  style: .cancel
) { _ in
  // 2
  invitationHandler(false, nil)
})
alertController.addAction(UIAlertAction(
  title: "Yes",
  style: .default
) { _ in
  // 3
  invitationHandler(true, self.session)
})
window.rootViewController?.present(alertController, animated: true)

In the code above, when the app receives an invitation, it displays an alert asking the user to accept or decline the job. There are three important things to focus on here:

  1. The context passed in gets converted to a string. When you get to the section about sending data, you’ll see how this happens. For now, you need to understand that any type of Data can be passed here and converted however you need.
  2. The invitationHandler closure is says whether the advertising device wants to accept a connection. This is how you cancel the invitation.
  3. If the user chooses to accept the connection, pass invitationHandler your current MCSession.

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

super.init()
nearbyServiceAdvertiser.delegate = self

Here, you set the delegate for your advertiser, which will show the alert when an invitation is sent.

You’re almost done setting up your device to advertise itself! Only a few more steps.

Add the following property to JobConnectionManager:

var isReceivingJobs: Bool = false {
  didSet {
    if isReceivingJobs {
      nearbyServiceAdvertiser.startAdvertisingPeer()
      print("Started advertising")
    } else {
      nearbyServiceAdvertiser.stopAdvertisingPeer()
      print("Stopped advertising")
    }
  }
}

You likely won’t want to have your device advertising itself as being available all the time. Here, you start or stop advertising based on the value of isReceivingJobs.

Binding the UI

Next, in JobListView.swift, add the following property:

@ObservedObject var jobConnectionManager: JobConnectionManager

Then replace the initializer with:

init(jobListStore: JobListStore = JobListStore()) {
  self.jobListStore = jobListStore
  jobConnectionManager = JobConnectionManager { job in
    jobListStore.jobs.append(job)
  }
}

In the code above, you’re doing two things:

  1. Adding an observed property to manage your advertising state.
  2. Initializing the same property. JobConnectionManager will send a job through this closure when it receives one. This code will append that received job to jobListStore, which will get used a little later on.

Finally, replace headerView in JobListView.swift with the following:

var headerView: some View {
  Toggle("Receive Jobs", isOn: $jobConnectionManager.isReceivingJobs)
}

By binding your Toggle to isReceivingJobs, your app will now turn advertising on and off.

Now remove the following property from JobListView.swift, as it’s no longer used:

@State private var isReceivingJobs = false

Build and run, and turn the RECEIVE JOBS toggle on and off. You’ll see the following in the Xcode Console:

Turning advertising toggle on and off

Debug statements showing advertising started and stopped

Discovering Devices

Now that you have the ability to advertise a device, the next step is to discover devices. Start by adding the following properties to JobConnectionManager.swift:

@Published var employees: [MCPeerID] = []
private var nearbyServiceBrowser: MCNearbyServiceBrowser

The first new property, employees, stores the devices discovered through the service you set up in the previous section.

The next property is MCNearbyServiceBrowser. This class handles all the work of discovering devices that have turned on advertising.

Next, in the initializer for JobConnectionManager, initialize nearbyServiceBrowser before the call to super:

nearbyServiceBrowser = MCNearbyServiceBrowser(
  peer: myPeerId, 
  serviceType: JobConnectionManager.service)

MCNearbyServiceBrowser looks similar to MCNearbyServiceAdvertiser. It requires a peer ID and a service. If both of these properties use JobConnectionManager.service, they’ll be able to discover and communicate with each other.

Add the following to the end of the JobConnectionManager.swift:

extension JobConnectionManager: MCNearbyServiceBrowserDelegate {
  func browser(
    _ browser: MCNearbyServiceBrowser,
    foundPeer peerID: MCPeerID,
    withDiscoveryInfo info: [String: String]?
  ) {
    // 1
    if !employees.contains(peerID) {
      employees.append(peerID)
    }
  }

  func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
    guard let index = employees.firstIndex(of: peerID) else { return }
    // 2
    employees.remove(at: index)
  }
}

Implementing MCNearbyServiceBrowserDelegate allows you to respond to events when peers or devices are discovered.

Two things are happening here:

  1. When the browser discovers a peer, it adds it to employees.
  2. Conversely, when the browser looses a peer, it removes it from the list.

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

nearbyServiceBrowser.delegate = self

This ensures your code from the previous step will be called as devices are discovered or lost.

Before you can set up the UI to show devices, add the following methods to enable and disable browsing in JobConnectionManager:

func startBrowsing() {
  nearbyServiceBrowser.startBrowsingForPeers()
}

func stopBrowsing() {
  nearbyServiceBrowser.stopBrowsingForPeers()
}

By default, your service browser won’t be browsing for peers. You can control the state by starting and stopping this service.

Rendering the List

To render the list, start by adding the following property to both JobView.swift and JobListRowView.swift:

@EnvironmentObject var jobConnectionManager: JobConnectionManager

Since you want to share model data between the job views, set up your property as an EnvironmentObject.

Next, in JobListView.swift, in body, replace the Section‘s body with the following:

ForEach(jobListStore.jobs) { job in
  JobListRowView(job: job)
    .environmentObject(jobConnectionManager)
}

This is now sharing your jobConnectionManager with JobListRowView.

You’ll need to set jobConnectionManager on one more view. In JobListRowView.swift, replace the destination of the NavigationLink with the following:

JobView(job: job).environmentObject(jobConnectionManager)

Like in the last step, it’s sharing jobConnectionManager with JobView.

Again, you’re almost there!

In JobView.swift, replace EmptyView(), which you’ll find in the last Section, with the following:

ForEach(jobConnectionManager.employees, id: \.self) { employee in
  HStack {
    Text(employee.displayName)
      .font(.headline)
    Spacer()
    Image(systemName: "arrowshape.turn.up.right.fill")
  }
}

This will populate the section with every employee found while browsing. It will then add and remove them to and from this list as they’re discovered or lost.

Finally, in JobView.swift, add the following to the end of the List after setting the navigation title:

.onAppear {
  jobConnectionManager.startBrowsing()
}
.onDisappear {
  jobConnectionManager.stopBrowsing()
}

When the job view appears, it’ll start browsing for peers, and it’ll stop when the view disappears. You’re all set!

Build and run on two different devices. You’ll see the following:

Two devices running with browsing and advertising

On one device, add a job, and then select this job from the main list.

On the other device, turn on RECEIVE JOBS. You’ll now see the following:

One device browsing for peers, the other advertising on

Finally, turn off RECEIVE JOBS from the second device, which removes the peer from the list of the first device:

One devices browsing for peers, with no peers discovered

Inviting Peers

Now the bulk of the work is complete, you’re ready to start sending jobs between devices. The first step is to wire up inviting devices to connect to each other.

Open JobConnectionManager.swift and add a new property:

private var jobToSend: JobModel?

When two devices connect, it happens asynchronously and through delegate methods. This property will temporarily save the job you’re sending.

Next, add the following method to the class:

func invitePeer(_ peerID: MCPeerID, to job: JobModel) {
  // 1
  jobToSend = job
  // 2
  let context = job.name.data(using: .utf8)
  // 3
  nearbyServiceBrowser.invitePeer(
    peerID, 
    to: session,
    withContext: context,
    timeout: TimeInterval(120))
}

Here’s a breakdown of what you added, step by step:

  1. Save the job until it’s needed
  2. Create a context. This is serializing the job’s name into Data.
  3. Ask your service browser invite a peer using the other device’s peer ID, passing the serialized job name as the context

Inviting a peer from one device will prompt the other device to connect using advertiser(_:didReceiveInvitationFromPeer:withContext:invitationHandler:), which you added earlier on. The browsing device creates the context, and the advertising peer receives it to help it determine if it wants to accept the job.

In JobView.swift, add a tap gesture to the rows that display nearby devices by adding the following to the end of the HStack that makes up the row:

.onTapGesture {
  jobConnectionManager.invitePeer(employee, to: job)
}

Tapping a nearby device or peer from the list will trigger the invitation process.

Build and run. Again, create a job on one device, and turn on RECEIVE JOBS on the other. Select the job from the main list and select the nearby device. You’ll see an invitation show up on the second device, like below:

Showing an invitation prompt before sharing job

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

Seeing Connected Peers

The chat room isn’t helpful if you can’t see who’s joined. So in session(_:peer:didChange:), find the following cases:

case .connected:
  print("Connected")
case .notConnected:
  print("Not Connected")

Then replace them with code below:

case .connected:
  if !peers.contains(peerID) {
    // 1
    DispatchQueue.main.async {
      self.peers.insert(peerID, at: 0)
    }
    // 2
    if isHosting {
      sendHistory(to: peerID)
    }
  }
case .notConnected:
  DispatchQueue.main.async {
    // 3
    if let index = self.peers.firstIndex(of: peerID) {
      self.peers.remove(at: index)
    }
    // 4
    if self.peers.isEmpty && !self.isHosting {
      self.connectedToChat = false
    }
  }

In the previous section, you used this method to know when to send jobs. Here, you’re using it to do the following:

  1. If a user connects to the chat, add them to the list of peers.
  2. If you’re the host, send the entire chat history to the newly connected user. You’ll implement this later.
  3. When a person disconnects, remove them from the list of peers.
  4. If the host and all the other peers leave, end the session automatically.

Build and run on both devices, hosting on one and joining on the other. You’ll now see both users in the top of the chat, like below:

View peers at the top of the chat

Sending Data to Multiple Devices

Now you’re ready to start sending messages between users! In ChatConnectionManager.swift, replace send(_:) with the following code:

func send(_ message: String) {
  // 1
  let chatMessage = ChatMessage(
    displayName: myPeerId.displayName, 
    body: message)
  messages.append(chatMessage)
  // 2
  guard 
    let session = session,
    let data = message.data(using: .utf8),
    !session.connectedPeers.isEmpty 
  else { return }

  do {
    // 3
    try session.send(data, toPeers: session.connectedPeers, with: .reliable)
  } catch {
    print(error.localizedDescription)
  }
}

Here’s a breakdown of the code above:

  1. Create a ChatMessage with the relevant data needed for local use.
  2. Encode the message string.
  3. Use MCSession to send to all the connected peers.

Next, replace session(_:didReceive:fromPeer:) with the following:

func session(
  _ session: MCSession,
  didReceive data: Data,
  fromPeer peerID: MCPeerID
) {
  guard let message = String(data: data, encoding: .utf8) else { return }
  let chatMessage = ChatMessage(displayName: peerID.displayName, body: message)
  DispatchQueue.main.async {
    self.messages.append(chatMessage)
  }
}

Build and run on both devices, and create a chat session between the two. You’ll see the following:

Sending messages between devices

If you leave the chat and come back, you’ll see the chat history is gone. You can remedy this by sending the entire chat history to users as they join.

Replace sendHistory(to:) with the following:

func sendHistory(to peer: MCPeerID) {
  // 1
  let tempFile = URL(fileURLWithPath: NSTemporaryDirectory())
    .appendingPathComponent("messages.data")
  guard let historyData = try? JSONEncoder().encode(messages) else { return }
  // 2
  try? historyData.write(to: tempFile)
  // 3
  session?.sendResource(
    at: tempFile,
    withName: "Chat_History",
    toPeer: peer
  ) { error in
    if let error = error {
      print(error.localizedDescription)
    }
  }
}

This code will execute on the hosting device. Here’s what happens:

  1. Create the URL in your temporary directory.
  2. Write the chat history to a file at this URL.
  3. Use MCSession to send a resource instead of Data.

Since you’re not sending data directly, you’ll need to use a different delegate method of MCSessionDelegate. Replace session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:) with the following:

func session(
  _ session: MCSession,
  didFinishReceivingResourceWithName resourceName: String,
  fromPeer peerID: MCPeerID,
  at localURL: URL?,
  withError error: Error?
) {
  // 1
  guard 
    let localURL = localURL,
    let data = try? Data(contentsOf: localURL),
    // 2 
    let messages = try? JSONDecoder().decode([ChatMessage].self, from: data) 
  else { return }

  DispatchQueue.main.async {
    // 3
    self.messages.insert(contentsOf: messages, at: 0)
  }
}

This code executes on the peer connecting to the session. Here’s what’s happening:

  1. If the resource was successfully saved locally on your device, you receive the URL here.
  2. The content of the file is decoded into ChatMessage.
  3. The messages are inserted in your local messages, which display when you join the chat.

Build and run on both devices. Create a chat session and send messages between both devices, like below:

A chat session with messages sent

On the devices you used to join the session, choose Leave:

A chat session, with a user that left after messages were sent

Rejoin the same session, and you’ll see the entire chat history, as found below:

Chat session where history is sent to newly joining user.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

You should now feel comfortable using Multipeer Connectivity and sending communication between devices without the internet. There’s still more to learn, such as sending and receiving streams between devices.

You can learn more by checking out more of our iOS & Swift Tutorials or by referring to Apple’s documentation.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

9 ratings

More like this

Contributors

Comments