Home · iOS & Swift Tutorials

Getting Started with Core NFC

In this tutorial, you’ll learn how to use CoreNFC to connect wirelessly to other devices or NFC tags.

4.5/5 2 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

Near Field Communication (NFC) is a technology for short-range wireless devices to share data with other devices or trigger actions on those devices. Built using a radio frequency field, it allows devices that don’t have any power to store small pieces of data while also enabling other powered devices to read that data.

iOS and watchOS devices have had NFC hardware built into them for several years now. In fact, Apple Pay uses this technology to interact with payment terminals at stores. However, developers weren’t able to use NFC hardware until iOS 11.

Apple upped its NFC game in iOS 13 by introducing Core NFC. With this new technology, you can program iOS devices to interact with the connected world around them in new ways. This tutorial will show you some of the ways you can use this technology. Along the way, you’ll learn how to:

  • Write standard information to an NFC tag.
  • Read that information.
  • Save custom information to a tag.
  • Modify the data already found on a tag.
Important Note: To perform all the steps in this tutorial, you’ll need the following:
  • A physical iOS device
  • An Apple developer account
  • NFC hardware that you can read from and write to. Many online retailers carry NFC tags at reasonable prices. You can usually get a pack of NFC tags for around $10 USD. Look for something in the description that states it is programmable or lists its storage capacity, typically 300 to 500 bytes. Any device with this approximate capacity is more than acceptable for this tutorial.

Getting Started

To get started, download the tutorial projects using the Download Materials button at the top or bottom of this tutorial and open the starter project in the starter folder. With the project app, you’ll learn how to:

  • Set up an NFC tag as a “location”.
  • Scan a location tag to see its name and visitor log.
  • Add a visitor to a location tag.

Build and run. You’ll see the following:

App running after downloading materials

Writing to Your First Tag

To get started, select the NeatoCache project in the Project navigator. Then, go to Signing & Capability and select + Capability. Choose Near Field Communication Tag Reading from the list.

Showing how to add the NFC entitlement to your project
This will ensure your app’s provisioning profile is set up to use NFC.

Next, open your Info.plist and add the following entry:

  • Key: Privacy – NFC Scan Usage Description
  • Value: Use NFC to Read and Write data

You need this entry to communicate to your users what you’re using the NFC capability for, as well as to conform to Apple’s requirements about using NFC in your application.

Next, you’ll add a function that can perform the various NFC tasks your app will handle. Open NFCUtility.swift and add the following import and type aliases to the top of the file:

import CoreNFC

typealias NFCReadingCompletion = (Result<NFCNDEFMessage?, Error>) -> Void
typealias LocationReadingCompletion = (Result<Location, Error>) -> Void

You need to import CoreNFC to work with NFC. The type aliases provide the following functionality:

  • NFCReadingCompletion for completing generic tag reading tasks.
  • LocationReadingCompletion for reading a tag configured as a location

Next, add the following properties and method to NFCUtility:

// 1
private var session: NFCNDEFReaderSession?
private var completion: LocationReadingCompletion?

// 2
static func performAction(
  _ action: NFCAction,
  completion: LocationReadingCompletion? = nil
) {
  // 3
  guard NFCNDEFReaderSession.readingAvailable else {
    completion?(.failure(NFCError.unavailable))
    print("NFC is not available on this device")
    return
  }

  shared.action = action
  shared.completion = completion
  // 4
  shared.session = NFCNDEFReaderSession(
    delegate: shared.self,
    queue: nil,
    invalidateAfterFirstRead: false)
  // 5
  shared.session?.alertMessage = action.alertMessage
  // 6
  shared.session?.begin()
}

Don’t worry if you get a compilation error at this point due to not conforming to NFCNDEFReaderSessionDelegate, you’ll fix this momentarily.

Here’s what you just did:

  1. You add session and completion properties to store the active NFC reading session and its completion block.
  2. Add a static function as an entry point for your NFC reading and writing tasks. You’ll use singleton-styled access to this function and to NFCUtility, in general.
  3. Make sure the device supports NFC reading. Otherwise, complete with an error.
  4. Create an NFCNDEFReaderSession, which represents the active reading session. You also set the delegate to be notified of the various events of NFC reading session.
  5. You set the alertMessage property on the session so that it displays that text to the user, within the NFC modal.
  6. Start the reading session. When called, a modal will present to the user with any instructions you set in the previous step.

Understanding NDEF

Notice that the code above introduces another acronym, NDEF, which stands for NFC Data Exchange Format. It’s a standardized format for writing to or reading from an NFC device. The two pieces of NDEF you’ll use are:

  • NDEF Record: This contains your payload value, such as a string, URL or custom data. It also contains information about that payload value, like length and type. This information is the NFCNDEFPayload within CoreNFC.
  • NDEF Message: This is the data structure that holds NDEF records. There can be one or more NDEF records within an NDEF message.

Detecting Tags

Now that you’ve set up your NFCReaderSession, it’s time to conform NFCUtility as its delegate, so you can get notified of the various events that occur during a reading session.

Add the following code to the bottom of NFCUtility.swift:

// MARK: - NFC NDEF Reader Session Delegate
extension NFCUtility: NFCNDEFReaderSessionDelegate {
  func readerSession(
    _ session: NFCNDEFReaderSession,
    didDetectNDEFs messages: [NFCNDEFMessage]
  ) {
    // Not used
  }
}

You’ll add more to this extension in just a second, but note here that you won’t do anything with readerSession(_:didDetectNDEFs:) in this tutorial. You’re only adding it here since it’s mandatory to conform to the delegate protocol.

The more you interact with NFC technology, the more you’ll see the possibility of encountering errors at various stages of the reading and writing process. Add the following method to your new extension to capture those errors:

private func handleError(_ error: Error) {
  session?.alertMessage = error.localizedDescription
  session?.invalidate()
}

The first line of code should look familiar to you. It will present the error message to the user in the NFC modal view. In the event of an error, you’ll also invalidate the session to terminate it and allow the user to interact with the app again.

Next, add the following method to the extension to deal with errors from your NFC reading session:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didInvalidateWithError error: Error
) {
  if let error = error as? NFCReaderError,
    error.code != .readerSessionInvalidationErrorFirstNDEFTagRead &&
      error.code != .readerSessionInvalidationErrorUserCanceled {
    completion?(.failure(NFCError.invalidated(message: 
      error.localizedDescription)))
  }

  self.session = nil
  completion = nil
}

Adding this delegate method will clear up any compilation errors you’ve encountered up to this point.

Finally, add this last method to your extension to deal with the possible detection of NFC tags:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didDetect tags: [NFCNDEFTag]
) {
  guard 
    let tag = tags.first,
    tags.count == 1 
    else {
      session.alertMessage = """
        There are too many tags present. Remove all and then try again.
        """
      DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) {
        session.restartPolling()
      }
      return
  }
}

Here, you implement the method that you’ll call when your session detects that you scanned a tag.

Typically, you would expect users to have only one tag close enough to their phone, but you should account for multiple tags in close proximity. If you detect this, you’ll stop the scan and alert the user. After showing the message, you’ll restart the reading session and let your user try again.

Handling the Tag

Once you know you have one tag, you’ll probably want to do something with it. Add the following code after your guard statement within readerSession(_:didDetect:):

// 1
session.connect(to: tag) { error in
  if let error = error {
    self.handleError(error)
    return
  }

  // 2
  tag.queryNDEFStatus { status, _, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 3
    switch (status, self.action) {
    case (.notSupported, _):
      session.alertMessage = "Unsupported tag."
      session.invalidate()
    case (.readOnly, _):
      session.alertMessage = "Unable to write to tag."
      session.invalidate()
    case (.readWrite, .setupLocation(let locationName)):
      self.createLocation(name: locationName, with: tag)
    case (.readWrite, .readLocation):
      return
    default:
      return
    }
  }
}

Here’s what you’re doing in the code above:

  1. Connect to the detected tag, using the current NCFNDEFReaderSession. You need to do this step to perform any reading or writing to the tag. Once connected, it will call its completion handler, with any error that might occur.
  2. Query the tag for its NDEF status to see if the NFC device is supported. The status must be readWrite for the purpose of your NeatoCache app.
  3. Switch over the status and the NFC action and determine what should be done based on their values. Here, you’re attempting to set up a tag to have a location name using createLocation(name:with:), which doesn’t exist yet, so you’ll encounter a compilation error. No worries though, you’ll add it in a moment. Similarly, the readLocation action is also not handled yet.

Creating the Payload

Up to this point, you’ve worked with finding a tag, connecting to it and querying its status. To finish setting up writing to a tag, add the following block of code to the end of NFCUtility.swift:

// MARK: - Utilities
extension NFCUtility {
  func createLocation(name: String, with tag: NFCNDEFTag) {
    // 1
    guard let payload = NFCNDEFPayload
      .wellKnownTypeTextPayload(string: name, locale: Locale.current) 
      else {
        handleError(NFCError.invalidated(message: "Could not create payload"))
        return
    }

    // 2
    let message = NFCNDEFMessage(records: [payload])

    // 3
    tag.writeNDEF(message) { error in
      if let error = error {
        self.handleError(error)
        return
      }

      self.session?.alertMessage = "Wrote location data."
      self.session?.invalidate()
      self.completion?(.success(Location(name: name)))
    }
  }
}

Here’s what you’re doing in the code above:

  1. Create a textual NFCNDEFPayload. As stated earlier, this is akin to an NDEF Record.
  2. Make a new NFCNDEFMessage with the payload so that you can save it to the NFC device.
  3. Finally, write the message to the tag.

Using NDEF Payload Types

NFCNDEFPayload supports several different types of data. In this example, you’re using wellKnownTypeTextPayload(string:locale:). This is a fairly simple data type that uses the string and the device’s current locale. A few of the other data types hold more complex information. Here’s the full list:

  • Empty
  • Well-Known
  • MIME media-type
  • Absolute URI
  • External
  • Unknown
  • Unchanged
  • Reserved
Note: This tutorial covers Well-Known and Unknown. To learn about the other types, check out the links listed at the end of this tutorial.

Also note that a type can have subtypes. Well-known, for example, has the subtypes of Text and URI.

You’re getting really close! All that’s left is to hook up your UI to your new code. Go to AdminView.swift and replace the following code:

Button(action: {
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

With this one:

Button(action: {
  NFCUtility.performAction(.setupLocation(locationName: self.locationName)) { _ in
    self.locationName = ""
  }
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

This will make a call to set up your location with the text found in the text field.

Build and run, switch to the Admin tab of the app, enter a name and select Save Location….

You’ll see the following:

Your app looking for an NFC tag to scan and write data to

Note: Remember, you’ll need to use a physical device and have an NFC tag that supports writing capabilities.

Once you place your phone on your NFC tag, you’ll see a message that your location was successfully saved.

The app successfully saving location data to an NFC tag

Reading the Tag

Great! Now that you now have an app that can write a string to a tag, you’re ready to build support for reading it. Go back to NFCUtility.swift and find the following code in readerSession(_:didDetect:).

case (.readWrite, .readLocation):
  return

Now, replace it with this:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

Time to implement that readLocation(from:) method. Add the following to the Utilities extension that contains createLocation(name:with:):

func readLocation(from tag: NFCNDEFTag) {
  // 1
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }
    // 2
    guard 
      let message = message,
      let location = Location(message: message) 
      else {
        self.session?.alertMessage = "Could not read tag data."
        self.session?.invalidate()
        return
    }
    self.completion?(.success(location))
    self.session?.alertMessage = "Read tag."
    self.session?.invalidate()
  }
}

This addition should look somewhat familiar to you, as it is very similar to how your wrote to the tag.

  1. First, you initiate reading a tag. If it can be read, it will return any messages it finds.
  2. Next, you attempt to create a Location out of the message data, if you have one. This uses a custom initializer that accepts a NFCNDEFMessage and pulls the name off it. If you’re curious, you can find that initializer in LocationModel.swift.

Finally, open VisitorView.swift, and in scanSection, replace the following code:

Button(action: {
}) {
  Text("Scan Location Tag…")
}

With this one:

Button(action: {
  NFCUtility.performAction(.readLocation) { location in
    self.locationModel = try? location.get()
  }
}) {
  Text("Scan Location Tag…")
}

You’re all set to read data from your tag. Build and run.

On the Visitors tab, tap Scan Location Tag…. You’ll see the following, with your location name now in the UI:

The app scanning and reading an NFC tag with location data

Writing Different Data Types

While writing strings may work perfectly for some use cases, you may find that you want to write other kinds of data to tags.

To prepare for this, add the following to NFCUtility.swift, within the Utilities extension:

private func read(
  tag: NFCNDEFTag,
  alertMessage: String = "Tag Read",
  readCompletion: NFCReadingCompletion? = nil
) {
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 1
    if let readCompletion = readCompletion,
       let message = message {
      readCompletion(.success(message))
    } else if 
      let message = message,
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) {
      // 2
      self.completion?(.success(location))
      self.session?.alertMessage = alertMessage
      self.session?.invalidate()
    } else {
      self.session?.alertMessage = "Could not decode tag data."
      self.session?.invalidate()
    }
  }
}

This new method for reading tags will be the entry point for most of your activities from here on out. As you can see, it still reads the tag, just like before. However, once it reads the tag, it will do one of two things:

  1. Call a completion handler and pass the message to it. This will be useful for chaining multiple NFC tasks together.
  2. Decode the payload so that you can parse the tag’s records. You’ll come back to this in a little bit.

Writing Custom Data Instead of Strings

At this point, you’re ready to convert the app from writing strings to a tag to writing custom data to a tag. Add the following to the Utilities extension:

private func createLocation(_ location: Location, tag: NFCNDEFTag) {
  read(tag: tag) { _ in
    self.updateLocation(location, tag: tag)
  }
}

This is your new function for creating a tag with a location. You can see that it uses the new read(tag:alsertMessage:readCompletion:) to start the process and calls a new function for updating a location on a tag, and also a new updateLocation(_:tag:) method which you’ll implement momentarily.

Since you’re replacing the way you write location information to a tag, remove createLocation(name:with:) at the beginning of the NFCUtility extension, as it is no longer needed. Also, update your code in readerSession(_:didDetect:) from this:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(name: locationName, with: tag)

…to the following:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(Location(name: locationName), tag: tag)

Next, add this method after createLocation(_:tag:):

private func updateLocation(
  _ location: Location,
  withVisitor visitor: Visitor? = nil,
  tag: NFCNDEFTag
) {
  // 1
  var alertMessage = "Successfully setup location."
  var tempLocation = location
  
  // 2
  let jsonEncoder = JSONEncoder()
  guard let customData = try? jsonEncoder.encode(tempLocation) else {
    self.handleError(NFCError.invalidated(message: "Bad data"))
    return
  }
  // 3
  let payload = NFCNDEFPayload(
    format: .unknown,
    type: Data(),
    identifier: Data(),
    payload: customData)
  // 4
  let message = NFCNDEFMessage(records: [payload])
}

Here’s what you’re doing in the code above:

  1. Create a default alert message and temporary location. You’ll come back to these later.
  2. Encode the Location struct passed in to the function. This will convert the model to raw Data. This is important, as it’s the way you write any custom type to an NFC tag.
  3. Create a payload that can handle your data. However, you now use unknown as the format. When doing this, you must set the type and identifier to empty Data, while the payload argument hosts the actual decoded model.
  4. Add the payload to a newly-created message.

Overall, this shouldn’t seem too different than when you saved a string to a tag — you’re just adding an extra step to convert a Swift data type to something a tag understands.

Checking Tag Capacity

To finish writing the data to the tag, add the next block of code within updateLocation(_:withVisitor:tag):

tag.queryNDEFStatus { _, capacity, _ in
  // 1
  guard message.length <= capacity else {
    self.handleError(NFCError.invalidPayloadSize)
    return
  }

  // 2
  tag.writeNDEF(message) { error in
    if let error = error {
      self.handleError(error)
      return
    }
    
    if self.completion != nil {
      self.read(tag: tag, alertMessage: alertMessage)
    }
  }
}

The closure above attempts to query the current NDEF status and then:

  1. Makes sure the device has enough storage to to store the location. Remember, NFC tags often have an extremely limited storage capacity compared to devices you might be familiar with.
  2. Write the message to the tag.

Build and run and set up a location as you did above. You can use the same tag from before if you wish, as your new code will overwrite any previously saved data.

Reading Your Custom Data

At this point, if you try to read your tag, you'll get an error. The data saved is no longer a well-known type. To fix this, replace the following code in readerSession(_:didDetect:):

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

...with this:

case (.readWrite, .readLocation):
  self.read(tag: tag)

Build and run and scan your tag. Because you're calling read(tag:alertMessage:readCompletion:) without any completion blocks, it will decode the data found in the message's first record.

Modifying Content

The last requirement for this app is to save a log of people that have visited this location. Your app has an unused feature already present in the UI that allows users to enter their name and add it to a tag. The work you've done so far will make the rest of the setup trivial. You can already read and write data to a tag, so modifying it should be a breeze for you.

In NFCUtility.swift, add this code to updateLocation(_:withVisitor:tag:) just after you create tempLocation:

if let visitor = visitor {
  tempLocation.visitors.append(visitor)
  alertMessage = "Successfully added visitor."
}

In the code above, you check to see if a visitor was provided. If so, you add it to the location's visitors array.

Next, add the following method to the Utilities extension:

private func addVisitor(_ visitor: Visitor, tag: NFCNDEFTag) {
  read(tag: tag) { message in
    guard 
      let message = try? message.get(),
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) 
      else {
        return
    }

    self.updateLocation(location, withVisitor: visitor, tag: tag)
  }
}

This new method will read a tag, get the message from it, and attempt to decode the Location on it.

Next, in readerSession(_:didDetect:), add a new case to your switch statement:

case (.readWrite, .addVisitor(let visitorName)):
  self.addVisitor(Visitor(name: visitorName), tag: tag)

If the user specifically wants to add a visitor, you'll call the function you added in the previous step.

All that remains is to update VisitorView.swift. In visitorSection, replace the following code:

Button(action: {
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

With this one:

Button(action: {
  NFCUtility
    .performAction(.addVisitor(visitorName: self.visitorName)) { location in
      self.locationModel = try? location.get()
      self.visitorName = ""
    }
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

Build and run, then go to the Visitors tab. Enter your name, then select Add To Tag…. After scanning, you'll see the updated location with a list of visitors found on the tag.

A scanned location tag showing location name an a list of visitors

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.

Now you should be familiar with the basics of Core NFC. There's still a lot of things to do with the framework that were not even mentioned in this tutorial. For example, you can add background reading of tags, which can provide a way for your users to interact with your app without having to open it. If you've used Apple's Shortcuts app to automate your smart home devices, this should seem familiar to you. You can find out more about doing this here: Adding Support for Background Tag Reading.

To learn more, checkout some of these great resources:

Apple's Core NFC Documentation is your go-to resource for everything Apple supports within the NFC spec.

The NFC Forum Homepage is the place for all the information you need about NFC in general, and the specifications it defines.

If you have any questions about this tutorial, please join in the forum discussion below!

Average Rating

4.5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments