In my previous article SwiftUI: Read/Write NFC Tags with Custom JSON Payload, I have shared with you the basics use Core NFC to read and write NFC Tags in SwiftUI.

However, that’s not really user friendly. Imagine that they have to open the app and tap on that scan button every time they want to scan something! We can do better than that by adding support for Background Tag Reading.

On iPhones that support background tag reading (that is iPhone XS and later), the system scans for and reads NFC data without requiring users to scan tags using an app. The system displays a pop-up notification each time it reads a new tag. After the user taps the notification, the system delivers the tag data to the appropriate app.

Do note that they are situations when the display is on and background tag reading is unavailable.

The device has never been unlocked.A Core NFC reader session is in progress.Apple Pay Wallet is in use.The camera is in use.Airplane mode is enabled.

In this article, let’s see how we can

make our App support background tag readingProcess the Custom JSON payload stored in the tagPass the payload data to a View

I have uploaded the source code we use in this article to GitHub! Grab it and let’s get started!

Prerequisite

This article assumes that you have already enabled Near Field Communication Tag Reading and added support for universal link. If not please feel free to refer to my previous articles

SwiftUI: Read/Write NFC Tags with Custom JSON Payload: for enabling Near Field Communication Tag Reading; andSwift/iOS: Support Universal Link (Host/Test Locally & on AWS) to add support for Universal link and set up your hosting associated domain. You can host it and test it out locally with ease!

Here is the start up code. It is basically where we left off previously in Read/Write NFC Tags with Custom JSON Payload with some minor modifications so let me just share it with you here really quick to make sure we are on the same line.

NFCManager

import Foundation
import CoreNFC

struct NFCDataModel: Codable {
var id: String
var favorite: String
}

class NFCManager: NSObject, NFCNDEFReaderSessionDelegate, ObservableObject {
private var readerSession: NFCNDEFReaderSession?
private var sessionMode: NFCSessionMode = .scan

@Published var message: String = “”

enum NFCSessionMode {
case scan
case write
}

enum NFCError: Error {
case recordCreation
}

func scan() {
guard NFCNDEFReaderSession.readingAvailable else {
print(“This device doesn’t support tag scanning. “)
return
}

self.sessionMode = .scan
readerSession = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: true)
readerSession?.alertMessage = “Get Closer to the Tag to Scan!”
readerSession?.begin()

}

func write() {
guard NFCNDEFReaderSession.readingAvailable else {
print(“This device doesn’t support tag scanning. “)
return
}

self.sessionMode = .write
readerSession = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: true)
readerSession?.alertMessage = “Get Closer to the Tag to Write!”
readerSession?.begin()
}

// MARK: delegate methods
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
// Error handling
print(“didInvalidateWithError: (error)”)
if let readerError = error as? NFCReaderError {
if (readerError.code != .readerSessionInvalidationErrorFirstNDEFTagRead)
&& (readerError.code != .readerSessionInvalidationErrorUserCanceled) {
DispatchQueue.main.async {
self.message = “Session invalidate with error: (error.localizedDescription)”
}
}
}
}

func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]){ }

func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [any NFCNDEFTag]) {
let retryInterval = DispatchTimeInterval.milliseconds(500)

if tags.count > 1 {
// Restart polling in 500 milliseconds.
session.alertMessage = “More than 1 tag is detected. Please remove all tags and try again.”
DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
session.restartPolling()
})
return
}

guard let tag = tags.first else {
print(“not able to get the first tag”)
session.alertMessage = “not able to get the first tag, please try again.”
DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
session.restartPolling()
})
return
}

DispatchQueue.main.async {
self.message = “”
}

Task {

do {
try await session.connect(to: tag)
let (status, _) = try await tag.queryNDEFStatus()

switch status {
case .notSupported:
session.alertMessage = “Tag is not NDEF compliant.”
case .readOnly:
if (sessionMode == .scan) {
print(“reading!”)
let ndefMessage = try await tag.readNDEF()
let processedMessage = try processNFCNDEFMessage(ndefMessage)
DispatchQueue.main.async {
self.message = processedMessage
}
} else {
session.alertMessage = “Tag is read only.”
}
case .readWrite:

if (sessionMode == .scan) {
print(“reading!”)
let ndefMessage = try await tag.readNDEF()
let processedMessage = try processNFCNDEFMessage(ndefMessage)
DispatchQueue.main.async {
self.message = processedMessage
}
} else {
let message = try createNFCNDEFMessage()
try await tag.writeNDEF(message)
}

@unknown default:
session.alertMessage = “Unknown NDEF tag status.”
}

session.invalidate()

} catch(let error) {
print(“failed with error: (error.localizedDescription)”)
session.alertMessage = “Failed to read/write tags.”
session.invalidate()
}
}
}

private func createNFCNDEFMessage() throws -> NFCNDEFMessage {

let dataModel = NFCDataModel(id: “itsuki in (Date())!”, favorite: “Pikachu x (Int.random(in: 1..<100))”)
let data = try JSONEncoder().encode(dataModel)
print(String(data: data, encoding: .utf8) ?? “Bad data”)
guard let type = “application/json”.data(using: .utf8) else {throw NFCError.recordCreation}
let payload = NFCNDEFPayload(format: .media, type: type, identifier: Data(), payload: data, chunkSize: 0)
let message = NFCNDEFMessage(records: [payload])
return message

}

func processNFCNDEFMessage(_ message: NFCNDEFMessage) throws -> String{
let records = message.records
var message = “”

for record in records {
print(record.typeNameFormat.description)
switch record.typeNameFormat {
case .nfcWellKnown:
if let url = record.wellKnownTypeURIPayload() {
message += “url: (url.absoluteString). “
}
let (text, locale) = record.wellKnownTypeTextPayload()
if let text = text, let locale = locale {
message += “Text: (text) with Locale: (locale). “
}

case .absoluteURI:
if let text = String(data: record.payload, encoding: .utf8) {
message += “absoluteURI: (text). “
}
case .media:
let type = record.type
print(String(data: type, encoding: .utf8) ?? “type unavailable”)

let data = record.payload
let dataString = String(data: data, encoding: .utf8)
print(dataString ?? “data unavailable”)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let result = try decoder.decode(NFCDataModel.self, from: data)
print(“(result.id): (result.favorite)”)

message += “Json Data: (result.id) loves (result.favorite). “

} catch (let error) {
print(“decode fail with error: (error)”)
throw error
}

case .nfcExternal, .empty, .unknown, .unchanged:
continue
@unknown default:
continue
}

print(“———“)

}
print(“—————————“)

return message
}

}

extension NFCTypeNameFormat: CustomStringConvertible {
public var description: String {
switch self {
case .nfcWellKnown: return “NFC Well Known type”
case .media: return “Media type”
case .absoluteURI: return “Absolute URI type”
case .nfcExternal: return “NFC External type”
case .unknown: return “Unknown type”
case .unchanged: return “Unchanged type”
case .empty: return “Empty payload”
@unknown default: return “Invalid data”
}
}
}

NFCView

import SwiftUI

struct NFCView: View {
@ObservedObject private var readerManager = NFCManager()

var body: some View {
VStack(spacing: 20) {

HStack(spacing: 30) {
Button(action: {
readerManager.scan()
}, label: {
Text(“Scan!”)
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))

Button(action: {
readerManager.write()
}, label: {
Text(“Write!”)
})
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))

}

Spacer()
.frame(height: 30)

if (!readerManager.message.isEmpty) {
Text(readerManager.message)
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}

}
.padding(.top, 100)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color.gray.opacity(0.2))
}
}

#Preview {
NFCView()
}

Overview

Just a quick overview of how the entire logic will look like.

Basically, our NFC Tag should contain a message with (at least) the following two records.

An URI record (that is typeNameFormat equal to NFCTypeNameFormat.nfcWellKnown and type equal to “U”) for the universal link of our App. This will enable the system to launch (or bring to the foreground) the app associated with the universal link after the user taps the notification.A data record with the our Custom JSON Payload to pass from the Tag to our App

Do note that

If there are no installed apps associated with the universal link, the system opens the link in Safari.If there is more than one URI record, the system uses the first one.

Write the Records to Tag

Let’s first write the records needed to our tag. You can use a third-party App such as NFC Tools, but I will personally recommend to modify the createNFCNDEFMessage in the NFCManager to the following, run the App, and press on that write button! This will help us to gain a better understanding of what we are actually doing here.

private func createNFCNDEFMessage() throws -> NFCNDEFMessage {
let dataModel = NFCDataModel(id: “itsuki in (Date())!”, favorite: “Pikachu x (Int.random(in: 1..<100))”)
let data = try JSONEncoder().encode(dataModel)
print(String(data: data, encoding: .utf8) ?? “Bad data”)
guard let type = “application/json”.data(using: .utf8) else {throw NFCError.recordCreation}
let payloadData = NFCNDEFPayload(format: .media, type: type, identifier: Data(), payload: data, chunkSize: 0)

guard let payloadUrl = NFCNDEFPayload.wellKnownTypeURIPayload(string: “https://852b-1-21-115-205.ngrok-free.app”) else {
throw NFCError.recordCreation
}

let message = NFCNDEFMessage(records: [payloadUrl, payloadData])
return message

}

Make sure to replace https://852b-1-21-115-205.ngrok-free.app with the universal link of your app!

We can actually open the App by tapping on the notification brought up by the system already at this point.

Handle Tag Delivery

Based on Apple, here is what we need to do to read in the information in a tag after the device scans an NFC tag while in background tag reading mode.

To handle the NDEF message read from the tag, implement the application(_:continue:restorationHandler:) method in your app delegate. The system calls this method to deliver the tag data to your app in an NSUserActivity object. The user activity has an activityType of NSUserActivityTypeBrowsingWeb, and the tag data is available in the ndefMessagePayload property.

Not really true for an SwiftUI App.

There are two cases we need to consider and handle separately.

App is killed (that is in background app state)App is inactive

Let’s check it out!

Add SceneDelegate

To handle the case where the user tapped on the notification brought up by the system when a tag is read and our app is killed, we will be doing it through Scene Delegate.

And Obviously, we don’t have it built-in for a SwiftUI App.

I will be adding my SceneDelegate by adding an additional AppDelegate, but you can also do it without AppDelegate by adding an entry to your info.plist. I have shared more detail about SwiftUI: Add App Delegate/Scene Delegate and Pass Data to Views previously, so please feel free to check it out!

Let’s first create our custom SceneDelegate class inhering from UIResponder and adopting the UIWindowSceneDelegate protocol. I have also had it conform to ObservableObject in addition so that we can pass data from it to our view with ease!

import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate, ObservableObject {
//…
}

We will be adding more implementations to it later!

We will then add our AppDelegate to assign our SceneDelegate to the UIScene.

import SwiftUI

class AppDelegate: NSObject, UIApplicationDelegate {

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let config = UISceneConfiguration(name: nil,
sessionRole: connectingSceneSession.role)
config.delegateClass = SceneDelegate.self
return config
}
}

We can then make our App use the AppDelegate by using the UIApplicationDelegateAdaptor, a property wrapper type to use to create a UIKit app delegate.

import SwiftUI
@main
struct NFCTageReaderApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
NFCView()
}
}
}

Note! There is no need to manually injected our SceneDelegate using .environmentObject. This is done for us (on the back) and we will be able to access it just like we would for any other EnvironmentObject.

Handle Tag Delivery when App is Killed

When the user tapped on the notification when our app is killed, the tag data will be delivered to the scene(_:willConnectTo:options:) method.

There are couple things we need to check on the userActivity before we further process them.

It has an activityType of NSUserActivityTypeBrowsingWebThe typeNameFormat of ndefMessagePayload contained in the userActivity is not NFCTypeNameFormat.empty. This is because for user activities not generated by background tag reading, for example, when user tap on the open in App option button when opening the url in Safari, the ndefMessagePayload returns a message containing only one NFCNDEFPayload record with a typeNameFormat of NFCTypeNameFormat.empty.

Here is how we will process it. We will be reusing our processNFCNDEFMessage function in the NFCManager.

import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate, ObservableObject {
@Published var message: String = “”

private let nfcManager = NFCManager()

func scene(_ scene: UIScene, willConnectTo
session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {

guard let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb else {
return
}

processUserActivity(userActivity)

}

func processUserActivity(_ userActivity: NSUserActivity) {
let ndefMessage = userActivity.ndefMessagePayload
// Confirm that the NSUserActivity object contains a valid NDEF message.

guard ndefMessage.records.count > 0,
ndefMessage.records[0].typeNameFormat != .empty else {
return
}

let message = try? nfcManager.processNFCNDEFMessage(ndefMessage)
DispatchQueue.main.async {
self.message = message ?? “”
}
}

}

I will simply display the data we received here as a string. Let’s modify our NFCView to the following. Obviously, as long as you have your data, you get to do anything you like with it!

import SwiftUI

struct NFCView: View {
@ObservedObject private var readerManager = NFCManager()
@EnvironmentObject var sceneDelegate: SceneDelegate

var body: some View {
VStack(spacing: 20) {

// …

if (!sceneDelegate.message.isEmpty) {
Text(sceneDelegate.message)
.foregroundStyle(Color.white)
.padding()
.background(RoundedRectangle(cornerRadius: 16))
}

}
.padding(.top, 100)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color.gray.opacity(0.2))
}
}

Let’s give our App A Run (And Kill it so that it is in background state when we tap on the notification)!

Handle Tag Delivery when App is Inactive

To handle universal link tapped when our app is in inactive state, we will be using the onContinueUserActivity(_:perform:) view modifier. This will register a handler to invoke in response to a user activity that our app receives.

I would like to reuse the processUserActivity function in our SceneDelegate so I will attach the modifier to the VStack of our NFCView like following.

struct NFCView: View {
@ObservedObject private var readerManager = NFCManager()
@EnvironmentObject var sceneDelegate: SceneDelegate

var body: some View {
VStack(spacing: 20) {

//…
}
// …
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: { userActivity in
sceneDelegate.processUserActivity(userActivity)
})
}
}

Yes! That’s it!

Let’s give our App another run (without killing it this time)! And here we go!

Thank you for reading!

That’s all I have for today!

Again, feel free to grab the demo code from GitHub!

Happy tagging!

SwiftUI: NFC Tags Background Reading with Custom JSON Payload and Pass Data to Views was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.