core-nfc

Read and write NFC tags using CoreNFC. Use when scanning NDEF tags, reading ISO7816/ISO15693/FeliCa/MIFARE tags, writing NDEF messages, handling NFC session…

INSTALLATION
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-nfc
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Project Configuration

  • Add the Near Field Communication Tag Reading capability in Xcode
  • Add NFCReaderUsageDescription to Info.plist with a user-facing reason string
  • Add the com.apple.developer.nfc.readersession.formats entitlement with the tag types your app reads (e.g., NDEF, TAG)
  • For ISO 7816 tags, add supported application identifiers to com.apple.developer.nfc.readersession.iso7816.select-identifiers in Info.plist

Device Requirements

NFC reading requires iPhone 7 or later. Always check for reader session

availability before presenting NFC UI.

import CoreNFC

guard NFCNDEFReaderSession.readingAvailable else {

    // Device does not support NFC or feature is restricted

    showUnsupportedMessage()

    return

}

Key Types

Type

Role

NFCNDEFReaderSession

Scans for NDEF-formatted tags

NFCTagReaderSession

Scans for ISO7816, ISO15693, FeliCa, MIFARE tags

NFCNDEFMessage

Collection of NDEF payload records

NFCNDEFPayload

Single record within an NDEF message

NFCNDEFTag

Protocol for interacting with an NDEF-capable tag

NDEF Reader Session

Use NFCNDEFReaderSession to read NDEF-formatted data from tags. This is the

simplest path for reading standard tag content like URLs, text, and MIME data.

import CoreNFC

final class NDEFReader: NSObject, NFCNDEFReaderSessionDelegate {

    private var session: NFCNDEFReaderSession?

    func beginScanning() {

        guard NFCNDEFReaderSession.readingAvailable else { return }

        session = NFCNDEFReaderSession(

            delegate: self,

            queue: nil,

            invalidateAfterFirstRead: false

        )

        session?.alertMessage = "Hold your iPhone near an NFC tag."

        session?.begin()

    }

    // MARK: - NFCNDEFReaderSessionDelegate

    func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {

        // Session is scanning

    }

    func readerSession(

        _ session: NFCNDEFReaderSession,

        didDetectNDEFs messages: [NFCNDEFMessage]

    ) {

        for message in messages {

            for record in message.records {

                processRecord(record)

            }

        }

    }

    func readerSession(

        _ session: NFCNDEFReaderSession,

        didInvalidateWithError error: Error

    ) {

        let nfcError = error as? NFCReaderError

        if nfcError?.code != .readerSessionInvalidationErrorFirstNDEFTagRead,

           nfcError?.code != .readerSessionInvalidationErrorUserCanceled {

            print("Session invalidated: \(error.localizedDescription)")

        }

        self.session = nil

    }

}

Reading with Tag Connection

For read-write operations, use the tag-detection delegate method to connect

to individual tags:

func readerSession(

    _ session: NFCNDEFReaderSession,

    didDetect tags: [any NFCNDEFTag]

) {

    guard let tag = tags.first else {

        session.restartPolling()

        return

    }

    session.connect(to: tag) { error in

        if let error {

            session.invalidate(errorMessage: "Connection failed: \(error)")

            return

        }

        tag.queryNDEFStatus { status, capacity, error in

            guard error == nil else {

                session.invalidate(errorMessage: "Query failed.")

                return

            }

            switch status {

            case .notSupported:

                session.invalidate(errorMessage: "Tag is not NDEF compliant.")

            case .readOnly:

                tag.readNDEF { message, error in

                    if let message {

                        self.processMessage(message)

                    }

                    session.invalidate()

                }

            case .readWrite:

                tag.readNDEF { message, error in

                    if let message {

                        self.processMessage(message)

                    }

                    session.alertMessage = "Tag read successfully."

                    session.invalidate()

                }

            @unknown default:

                session.invalidate()

            }

        }

    }

}

Tag Reader Session

Use NFCTagReaderSession when you need direct access to the native tag

protocol (ISO 7816, ISO 15693, FeliCa, or MIFARE).

final class TagReader: NSObject, NFCTagReaderSessionDelegate {

    private var session: NFCTagReaderSession?

    func beginScanning() {

        session = NFCTagReaderSession(

            pollingOption: [.iso14443, .iso15693],

            delegate: self,

            queue: nil

        )

        session?.alertMessage = "Hold your iPhone near a tag."

        session?.begin()

    }

    func tagReaderSessionDidBecomeActive(

        _ session: NFCTagReaderSession

    ) { }

    func tagReaderSession(

        _ session: NFCTagReaderSession,

        didDetect tags: [NFCTag]

    ) {

        guard let tag = tags.first else { return }

        session.connect(to: tag) { error in

            guard error == nil else {

                session.invalidate(

                    errorMessage: "Connection failed."

                )

                return

            }

            switch tag {

            case .iso7816(let iso7816Tag):

                self.readISO7816(tag: iso7816Tag, session: session)

            case .miFare(let miFareTag):

                self.readMiFare(tag: miFareTag, session: session)

            case .iso15693(let iso15693Tag):

                self.readISO15693(tag: iso15693Tag, session: session)

            case .feliCa(let feliCaTag):

                self.readFeliCa(tag: feliCaTag, session: session)

            @unknown default:

                session.invalidate(errorMessage: "Unsupported tag type.")

            }

        }

    }

    func tagReaderSession(

        _ session: NFCTagReaderSession,

        didInvalidateWithError error: Error

    ) {

        self.session = nil

    }

}

Writing NDEF Messages

Write NDEF data to a connected tag. Always check readWrite status first.

func writeToTag(

    tag: any NFCNDEFTag,

    session: NFCNDEFReaderSession,

    url: URL

) {

    tag.queryNDEFStatus { status, capacity, error in

        guard status == .readWrite else {

            session.invalidate(errorMessage: "Tag is read-only.")

            return

        }

        guard let payload = NFCNDEFPayload.wellKnownTypeURIPayload(

            url: url

        ) else {

            session.invalidate(errorMessage: "Invalid URL.")

            return

        }

        let message = NFCNDEFMessage(records: [payload])

        tag.writeNDEF(message) { error in

            if let error {

                session.invalidate(

                    errorMessage: "Write failed: \(error.localizedDescription)"

                )

            } else {

                session.alertMessage = "Tag written successfully."

                session.invalidate()

            }

        }

    }

}

NDEF Payload Types

Creating Common Payloads

// URL payload

let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(

    url: URL(string: "https://example.com")!

)

// Text payload

let textPayload = NFCNDEFPayload.wellKnownTypeTextPayload(

    string: "Hello NFC",

    locale: Locale(identifier: "en")

)

// Custom payload

let customPayload = NFCNDEFPayload(

    format: .nfcExternal,

    type: "com.example:mytype".data(using: .utf8)!,

    identifier: Data(),

    payload: "custom-data".data(using: .utf8)!

)

Parsing Payload Content

func processRecord(_ record: NFCNDEFPayload) {

    switch record.typeNameFormat {

    case .nfcWellKnown:

        if let url = record.wellKnownTypeURIPayload() {

            print("URL: \(url)")

        } else if let (text, locale) = record.wellKnownTypeTextPayload() {

            print("Text (\(locale)): \(text)")

        }

    case .absoluteURI:

        if let uri = String(data: record.payload, encoding: .utf8) {

            print("Absolute URI: \(uri)")

        }

    case .media:

        let mimeType = String(data: record.type, encoding: .utf8) ?? ""

        print("MIME type: \(mimeType), size: \(record.payload.count)")

    case .nfcExternal:

        let type = String(data: record.type, encoding: .utf8) ?? ""

        print("External type: \(type)")

    case .empty, .unknown, .unchanged:

        break

    @unknown default:

        break

    }

}

Background Tag Reading

On iPhone XS and later, iOS can read NFC tags in the background without

opening your app. To opt in:

  • Add associated domains or universal links that match the URL on your tags
  • Register your app for the tag's NDEF content type
  • Include your app's bundle ID in the tag's NDEF record

When a user taps a compatible tag, iOS displays a notification that opens

your app. Handle the tag data via NSUserActivity:

func scene(

    _ scene: UIScene,

    continue userActivity: NSUserActivity

) {

    guard userActivity.activityType ==

        NSUserActivityTypeBrowsingWeb else { return }

    if let message = userActivity.ndefMessagePayload {

        for record in message.records {

            processRecord(record)

        }

    }

}

Common Mistakes

DON'T: Forget the NFC entitlement

Without the com.apple.developer.nfc.readersession.formats entitlement,

session creation crashes at runtime.

// WRONG -- entitlement not added, crashes

let session = NFCNDEFReaderSession(

    delegate: self, queue: nil, invalidateAfterFirstRead: true

)

// CORRECT -- add entitlement in Signing & Capabilities first

// Then the same code works:

let session = NFCNDEFReaderSession(

    delegate: self, queue: nil, invalidateAfterFirstRead: true

)

DON'T: Skip the readingAvailable check

Attempting to create an NFC session on an unsupported device (iPad, iPod

touch, or iPhone 6s and earlier) crashes.

// WRONG

func scan() {

    let session = NFCNDEFReaderSession(

        delegate: self, queue: nil, invalidateAfterFirstRead: true

    )

    session.begin()

}

// CORRECT

func scan() {

    guard NFCNDEFReaderSession.readingAvailable else {

        showUnsupportedAlert()

        return

    }

    let session = NFCNDEFReaderSession(

        delegate: self, queue: nil, invalidateAfterFirstRead: true

    )

    session.begin()

}

DON'T: Ignore session invalidation errors

The session invalidates for multiple reasons. Distinguishing user cancellation

from real errors prevents false error alerts.

// WRONG -- shows error when user cancels

func readerSession(

    _ session: NFCNDEFReaderSession,

    didInvalidateWithError error: Error

) {

    showAlert("NFC Error: \(error.localizedDescription)")

}

// CORRECT -- filter expected invalidation reasons

func readerSession(

    _ session: NFCNDEFReaderSession,

    didInvalidateWithError error: Error

) {

    let nfcError = error as? NFCReaderError

    switch nfcError?.code {

    case .readerSessionInvalidationErrorUserCanceled,

         .readerSessionInvalidationErrorFirstNDEFTagRead:

        break  // Normal termination

    default:

        showAlert("NFC Error: \(error.localizedDescription)")

    }

    self.session = nil

}

DON'T: Hold a strong reference to a stale session

Once a session is invalidated, it cannot be restarted. Nil out your reference

and create a new session for the next scan.

// WRONG -- reusing invalidated session

func scanAgain() {

    session?.begin()  // Does nothing, session is dead

}

// CORRECT -- create a new session

func scanAgain() {

    session = NFCNDEFReaderSession(

        delegate: self, queue: nil, invalidateAfterFirstRead: false

    )

    session?.begin()

}

DON'T: Write without checking tag status

Writing to a read-only tag silently fails or produces confusing errors.

// WRONG -- writes without checking status

tag.writeNDEF(message) { error in

    // May fail on read-only tags

}

// CORRECT -- check status first

tag.queryNDEFStatus { status, capacity, error in

    guard status == .readWrite else {

        session.invalidate(errorMessage: "Tag is read-only.")

        return

    }

    tag.writeNDEF(message) { error in

        // Handle result

    }

}

Review Checklist

  • NFC capability added in Signing & Capabilities
  • NFCReaderUsageDescription set in Info.plist
  • com.apple.developer.nfc.readersession.formats entitlement configured with correct tag types
  • NFCNDEFReaderSession.readingAvailable checked before creating sessions
  • Session delegate set before calling begin()
  • Session reference set to nil after invalidation
  • didInvalidateWithError distinguishes user cancellation from actual errors
  • NDEF status queried before write operations
  • Tag capacity checked before writing large messages
  • ISO 7816 application identifiers listed in Info.plist if using NFCTagReaderSession
  • Background tag reading configured with associated domains if needed
  • Only one reader session active at a time

References

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card