Near Field Communication (NFC) is a short-range wireless technology that enables communication between two compatible devices when brought within a few centimeters of each other. This technology powers various applications, including contactless payments, data sharing, and access control, offering faster and more convenient transactions. NFC's ease of use eliminates the need for complex pairing processes, enabling seamless interactions between devices and making it accessible to a broad audience.
In this post, we will create a basic iOS application that reads from and writes to an NFC tag.
Requirements
- iOS Device Compatibility: You need to deploy it on a real iOS device running iOS 13 or later. All iPhone 7 models and newer can read and write NFC tags.
- NFC Tags: Ensure that the NFC tags you use are compatible with iOS. I’ve purchased these tags—double-check their compatibility if you decide to experiment with them.
Base project and NFC configuration
Setting up NFC on any iOS app requires a minimum of two steps. The first step is to set the ‘NFC scan usage description’ text message in Build settings (or in the Info.plist file if you’re working with an older iOS project).

The second enhancement is to add ‘Near Field Communication (NFC) Tag’ capability to the signing capabilities.

Finally setup entitlements for allowing working with NDEF tags:

NFC sample application
The app features a straightforward interface consisting of an input box for entering the value to be stored on the NFC tag, a button for reading, and another for writing. At the bottom, it displays the value retrieved from the tag.

From the coding perspective, the app serves as both a view and a manager for handling NFC operations. Below is an introduction to the NFC Manager:
final class NFCManager: NSObject, ObservableObject,
@unchecked Sendable {
@MainActor
static let shared = NFCManager()
@MainActor
@Published var tagMessage = ""
private var internalTagMessage: String = "" {
@Sendable didSet {
Task { [internalTagMessage] in
await MainActor.run {
self.tagMessage = internalTagMessage
}
}
}
}
var nfcSession: NFCNDEFReaderSession?
var isWrite = false
private var userMessage: String?
@MainActor override init() {
}
}
The code is compatible with Swift 6. I had to rollback the use of @GlobalActor
for this class because some delegated methods were directly causing the app to crash. The tagMessage
attribute, which holds the content of the NFC tag, is a @Published
property that is ultimately displayed in the view.
This attribute is marked with @MainActor
, but the Manager
operates in a different, isolated domain. To avoid forcing updates to this attribute on @MainActor
directly from any delegated method, I created a mirrored property, internalTagMessage
. This property resides in the same isolated domain as the NFC Manager. Whenever internalTagMessage
is updated, its value is then safely transferred to @MainActor
. This approach ensures that the delegate methods remain cleaner and avoids cross-domain synchronization issues.
// MARK :- NFCManagerProtocol
extension NFCManager: NFCManagerProtocol {
func startReading() async {
self.nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
self.isWrite = false
self.nfcSession?.begin()
}
func startWriting(message: String) async {
nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
isWrite = true
userMessage = message
nfcSession?.begin()
}
}
The NFCManagerProtocol
defines the operations requested by the view. Each time a new read or write operation is initiated, a new NFC NDEF reader session is started, and the relevant delegate methods are invoked to handle the operation.
// MARK :- NFCNDEFReaderSessionDelegate
extension NFCManager: NFCNDEFReaderSessionDelegate {
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
guard let tag = tags.first else { return }
session.connect(to: tag) { error in
if let error = error {
session.invalidate(errorMessage: "Connection error: \(error.localizedDescription)")
return
}
tag.queryNDEFStatus { status, capacity, error in
guard error == nil else {
session.invalidate(errorMessage: "Error checking NDEF status")
return
}
switch status {
case .notSupported:
session.invalidate(errorMessage: "Not compatible tat")
case .readOnly:
session.invalidate(errorMessage: "Tag is read-only")
case .readWrite:
if self.isWrite {
self.write(session: session, tag: tag)
} else {
self.read(session: session, tag: tag)
}
@unknown default:
session.invalidate(errorMessage: "Unknown NDEF status")
}
}
}
}
private func read(session: NFCNDEFReaderSession, tag: NFCNDEFTag) {
tag.readNDEF { [weak self] message, error in
if let error {
session.invalidate(errorMessage: "Reading error: \(error.localizedDescription)")
return
}
guard let message else {
session.invalidate(errorMessage: "No recrods found")
return
}
if let record = message.records.first {
let tagMessage = String(data: record.payload, encoding: .utf8) ?? ""
print(">>> Read: \(tagMessage)")
session.alertMessage = "ReadingSucceeded: \(tagMessage)"
session.invalidate()
self?.internalTagMessage = tagMessage
}
}
}
private func write(session: NFCNDEFReaderSession, tag: NFCNDEFTag) {
guard let userMessage = self.userMessage else { return }
let payload = NFCNDEFPayload(
format: .nfcWellKnown,
type: "T".data(using: .utf8)!,
identifier: Data(),
payload: userMessage.data(using: .utf8)!
)
let message = NFCNDEFMessage(records: [payload])
tag.writeNDEF(message) { error in
if let error = error {
session.invalidate(errorMessage: "Writing error: \(error.localizedDescription)")
} else {
print(">>> Write: \(userMessage)")
session.alertMessage = "Writing succeeded"
session.invalidate()
}
}
}
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
print( "Session Error: \(error.localizedDescription)")
}
}
readerSession(_:didDetectNDEFs:)
This method is a placeholder for handling detected NDEF messages. Currently, it doesn’t contain implementation logic.readerSession(_:didDetect:)
This method is triggered when NFC tags are detected. It connects to the first detected tag and determines its NDEF status (read/write capabilities). Depending on the status, it decides whether to read or write data using theread
orwrite
methods.readerSessionDidBecomeActive(_:)
This method is called when the NFC reader session becomes active. It has no custom logic here.readerSession(_:didInvalidateWithError:)
This method handles session invalidation due to errors, logging the error message.
Finally, deploying the app on a real device should exhibit the following behavior:
Store the text “Hello world!” in an NFC tag. Later, retrieve the text from the tag and display it at the bottom of the view.
Conclusions
This example takes a minimalist approach to demonstrate how easy it is to start experimenting with this technologyy.You can find source code used for writing this post in following repository.
References
- Core NFC
Apple Developer Documentation
- Core NFC Enhancements
WWDC 2020
- What's new in Core NFC
WWDC 2019