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

To successfully use this technology, two requirements must be met:
  1. 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.
  2. 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 the read or write 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

Copyright © 2024-2025 JaviOS. All rights reserved