
Core Nfc
- 2.6k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
core-nfc is an iOS skill for CoreNFC NDEF and native tag reader sessions with entitlements and write patterns.
About
The core-nfc skill documents read and write NFC tag workflows on iPhone using Apple's CoreNFC framework for Swift 6.3 and iOS 26 plus. Setup requires the Near Field Communication Tag Reading capability, NFCReaderUsageDescription in Info.plist, and com.apple.developer.nfc.readersession.formats entitlements with current TAG values rather than legacy NDEF. NFCNDEFReaderSession covers standard NDEF URLs, text, and MIME records, while NFCTagReaderSession exposes ISO7816, ISO15693, FeliCa, and MIFARE protocols with protocol-specific polling options. Examples walk delegate lifecycle methods, connect-to-tag flows, queryNDEFStatus for readOnly versus readWrite tags, wellKnownTypeURIPayload writes, and invalidate error handling. Device support starts at iPhone 7 with readingAvailable guards before showing NFC UI. Additional guidance covers ISO7816 application identifiers, FeliCa system codes without wildcards, background tag reading, common mistakes, and a review checklist. Apple documents NFCPaymentTagReaderSession for eligible EU payment AIDs instead of generic tag sessions.
- NFCNDEFReaderSession scans NDEF tags; NFCTagReaderSession handles ISO7816, FeliCa, and MIFARE.
- Entitlements use com.apple.developer.nfc.readersession.formats TAG values, not legacy NDEF.
- queryNDEFStatus distinguishes readOnly, readWrite, and notSupported before reads or writes.
- readingAvailable must be checked before creating sessions on supported iPhone hardware.
- Payment AIDs belong on NFCPaymentTagReaderSession per Apple EU payment guidance.
Core Nfc by the numbers
- 2,569 all-time installs (skills.sh)
- +110 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #81 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
core-nfc capabilities & compatibility
- Capabilities
- nfcndefreadersession delegate lifecycle and tag · nfctagreadersession multi protocol polling and t · ndef message construction and wellknowntypeuripa · entitlement and info.plist setup for nfcreaderus · background tag reading and review checklist guid
- Use cases
- frontend · api development
- Platforms
- macOS
What core-nfc says it does
Read and write NFC tags on iPhone using the CoreNFC framework.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-nfcAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I read and write NFC tags with CoreNFC in a Swift iOS app?
Implement CoreNFC NDEF and tag reader sessions, write NDEF payloads, configure entitlements, and handle background tag reading in Swift iOS apps.
Who is it for?
iOS apps scanning NDEF tags, writing URLs, or accessing ISO7816, FeliCa, and MIFARE tags.
Skip if: Skip for Android NFC or server-only tag provisioning without on-device CoreNFC.
When should I use this skill?
User mentions CoreNFC, NDEF reader session, NFC entitlements, or background tag reading.
What you get
Configured NFC sessions, entitlements, and delegate code for NDEF or protocol-specific tag access.
- entitlement review
- nfc reader session plan
- aid configuration checklist
Files
CoreNFC
Read and write NFC tags on iPhone using the CoreNFC framework. Covers NDEF reader sessions, tag reader sessions, NDEF message construction, entitlements, and background tag reading. Targets Swift 6.3 / iOS 26+.
Contents
- Setup
- NDEF Reader Session
- Tag Reader Session
- Writing NDEF Messages
- NDEF Payload Types
- Background Tag Reading
- Common Mistakes
- Review Checklist
- References
Setup
Project Configuration
1. Add the Near Field Communication Tag Reading capability in Xcode 2. Add NFCReaderUsageDescription to Info.plist with a user-facing reason string 3. Add the com.apple.developer.nfc.readersession.formats entitlement with the current TAG value; do not add legacy NDEF 4. For ISO 7816 tags, add supported application identifiers to com.apple.developer.nfc.readersession.iso7816.select-identifiers in Info.plist 5. For FeliCa tags, add supported system codes to com.apple.developer.nfc.readersession.felica.systemcodes; do not use wildcard system codes
Device Requirements
NFC reading requires iPhone 7 or later. Always check for reader session availability before creating NFC UI or sessions. Use the concrete reader session type you are about to create.
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).
Polling options are protocol-specific: .iso14443 detects ISO 7816-compatible and MIFARE tags, .iso15693 detects ISO 15693 tags, and .iso18092 detects FeliCa tags. Do not use NFCTagReaderSession for payment-related AIDs; Apple documents NFCPaymentTagReaderSession for eligible EU payment use cases.
final class TagReader: NSObject, NFCTagReaderSessionDelegate {
private var session: NFCTagReaderSession?
func beginScanning() {
guard NFCTagReaderSession.readingAvailable else { return }
session = NFCTagReaderSession(
pollingOption: [.iso14443, .iso15693, .iso18092],
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. The NDEF message must contain a URI record (typeNameFormat == .nfcWellKnown, type U). If there are multiple URI records, the system uses the first one.
For app-specific routing, write a universal link to the tag and configure the Associated Domains capability for that domain. Background tag reading also supports specific system URL schemes such as web, email, SMS, telephone, FaceTime, Maps, and HomeKit setup. It does not support custom URL schemes, and the system does not route by bundle ID or arbitrary NDEF content type.
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 }
let message = userActivity.ndefMessagePayload
guard message.records.first?.typeNameFormat != .empty else { return }
for record in message.records {
processRecord(record)
}
}Common Mistakes
DON'T: Use stale or missing NFC entitlements
Without the com.apple.developer.nfc.readersession.formats entitlement, reader sessions cannot access NFC hardware. Use the current TAG value for Core NFC reader sessions; do not copy older examples that add NDEF.
DON'T: Skip the readingAvailable check
Creating an NFC session on an unsupported or restricted device fails before the scan UI can do useful work.
Check NFCNDEFReaderSession.readingAvailable or NFCTagReaderSession.readingAvailable before creating the matching session.
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
- [ ]
NFCReaderUsageDescriptionset in Info.plist - [ ]
com.apple.developer.nfc.readersession.formatsentitlement usesTAG, not legacyNDEF - [ ]
NFCNDEFReaderSession.readingAvailableorNFCTagReaderSession.readingAvailablechecked before creating sessions - [ ] Session delegate set before calling
begin() - [ ] Session reference set to nil after invalidation
- [ ]
didInvalidateWithErrordistinguishes 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 - [ ] FeliCa system codes listed in Info.plist when polling
.iso18092 - [ ] Background tag reading uses a URI NDEF record and universal links or supported system URL schemes
- [ ] Custom URL schemes, bundle IDs, or arbitrary NDEF content types are not used for background routing
- [ ] Payment-related AIDs are routed away from
NFCTagReaderSession - [ ] Only one reader session active at a time
References
- Extended patterns (ISO 7816 commands, multi-tag scanning, NDEF locking): references/nfc-patterns.md
- Core NFC framework
- NFCNDEFReaderSession
- NFCTagReaderSession
- NFCNDEFMessage
- NFCNDEFPayload
- NFCNDEFTag
- NFCNDEFReaderSessionDelegate
- NFCTagReaderSessionDelegate
- Building an NFC Tag-Reader App
- Adding Support for Background Tag Reading
- Near Field Communication Tag Reader Session Formats Entitlement
- ISO7816 application identifiers for NFC Tag Reader Session
- ISO18092 system codes for NFC Tag Reader Session
{
"skill_name": "core-nfc",
"evals": [
{
"id": 0,
"name": "entitlement-setup-review",
"prompt": "Review this Core NFC setup before we hand it to the iOS team. The app needs to scan NDEF tags, read ISO 7816 identity cards by AID, and read a FeliCa transit tag. The draft entitlements include com.apple.developer.nfc.readersession.formats = [\"NDEF\", \"TAG\"], NFCReaderUsageDescription, and one ISO7816 AID. What should we change or verify?",
"expected_output": "A source-grounded setup review that corrects stale Core NFC entitlements and covers ISO 7816 and FeliCa configuration.",
"files": [],
"expectations": [
"States that the current `com.apple.developer.nfc.readersession.formats` value should use `TAG` and should not include legacy `NDEF`.",
"Requires a non-empty `NFCReaderUsageDescription` in Info.plist.",
"Requires `com.apple.developer.nfc.readersession.iso7816.select-identifiers` entries for ISO 7816 AIDs.",
"Requires `com.apple.developer.nfc.readersession.felica.systemcodes` entries for FeliCa and says wildcard system codes are not allowed.",
"Tells the team to check `NFCNDEFReaderSession.readingAvailable` or `NFCTagReaderSession.readingAvailable` before creating sessions."
]
},
{
"id": 1,
"name": "native-tag-reader-boundary",
"prompt": "Design a concise Swift Core NFC tag scanner that can read ISO 7816, ISO 15693, MIFARE, and FeliCa tags. Also call out any payment-card or multi-tag pitfalls. Keep it as implementation guidance and snippets, not a full app.",
"expected_output": "Implementation guidance that uses NFCTagReaderSession correctly, maps polling options to tag families, and preserves payment-card boundaries.",
"files": [],
"expectations": [
"Uses `NFCTagReaderSession` for native ISO 7816, ISO 15693, MIFARE, and FeliCa access.",
"Checks `NFCTagReaderSession.readingAvailable` before creating the session.",
"Maps `.iso14443` to ISO 7816-compatible and MIFARE tags, `.iso15693` to ISO 15693, and `.iso18092` to FeliCa.",
"Connects to the detected tag before sending protocol commands and switches over `NFCTag` cases.",
"Handles multiple detected tags by asking the user to remove extras and restarting polling rather than arbitrarily using every tag.",
"States that `NFCTagReaderSession` does not support payment-related AIDs and routes eligible EU payment use cases to `NFCPaymentTagReaderSession`."
]
},
{
"id": 2,
"name": "background-tag-routing",
"prompt": "A product manager wants an NFC tag to wake the app in the background by putting our bundle ID and a custom content type in the NDEF record. They also asked whether a custom myapp:// URL is fine. Write the corrected iOS implementation checklist.",
"expected_output": "A background tag reading checklist that corrects bundle-ID/content-type misconceptions and uses URI records and universal links.",
"files": [],
"expectations": [
"Says background tag reading looks for an NDEF URI record with type name format `.nfcWellKnown` and type `U`.",
"States that if multiple URI records exist, iOS uses the first URI record.",
"Uses universal links with Associated Domains for app-specific routing.",
"States that custom URL schemes are not supported for background tag reading.",
"Rejects routing by bundle ID or arbitrary NDEF content type.",
"Handles delivery through `NSUserActivityTypeBrowsingWeb` and `NSUserActivity.ndefMessagePayload`, including an empty-record guard for non-background activities."
]
}
]
}
CoreNFC Extended Patterns
Overflow reference for the core-nfc skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- ISO 7816 APDU Commands
- ISO 15693 Tag Reading
- MIFARE Tag Operations
- FeliCa Tag Operations
- Multi-Record NDEF Messages
- NDEF Tag Locking
- SwiftUI NFC Scanner
- Error Handling Reference
ISO 7816 APDU Commands
Send APDU commands to ISO 7816-compliant smart cards and tags:
import CoreNFC
func readISO7816(
tag: NFCISO7816Tag,
session: NFCTagReaderSession
) {
// Select application by AID
let selectAID = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xA4,
p1Parameter: 0x04,
p2Parameter: 0x00,
data: Data([0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]),
expectedResponseLength: -1
)
tag.sendCommand(apdu: selectAID) { data, sw1, sw2, error in
guard error == nil, sw1 == 0x90, sw2 == 0x00 else {
session.invalidate(errorMessage: "Select failed.")
return
}
// Read binary
let readBinary = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xB0,
p1Parameter: 0x00,
p2Parameter: 0x00,
data: Data(),
expectedResponseLength: 256
)
tag.sendCommand(apdu: readBinary) { data, sw1, sw2, error in
guard error == nil else {
session.invalidate(errorMessage: "Read failed.")
return
}
print("Read \(data.count) bytes, SW: \(sw1) \(sw2)")
session.alertMessage = "Tag read successfully."
session.invalidate()
}
}
}Common APDU Commands
| Command | CLA | INS | Description |
|---|---|---|---|
| SELECT | 0x00 | 0xA4 | Select an application or file |
| READ BINARY | 0x00 | 0xB0 | Read data from a transparent file |
| UPDATE BINARY | 0x00 | 0xD6 | Write data to a transparent file |
| READ RECORD | 0x00 | 0xB2 | Read a record from a record-oriented file |
| GET DATA | 0x00 | 0xCA | Retrieve a data object |
ISO 15693 Tag Reading
func readISO15693(
tag: NFCISO15693Tag,
session: NFCTagReaderSession
) {
// Read a single block
tag.readSingleBlock(
requestFlags: [.highDataRate, .address],
blockNumber: 0
) { data, error in
guard let data, error == nil else {
session.invalidate(errorMessage: "Read failed.")
return
}
print("Block 0: \(data.map { String(format: "%02x", $0) }.joined())")
}
// Read multiple blocks
tag.readMultipleBlocks(
requestFlags: [.highDataRate, .address],
blockRange: NSRange(location: 0, length: 4)
) { blocks, error in
guard let blocks, error == nil else { return }
for (index, block) in blocks.enumerated() {
print("Block \(index): \(block.count) bytes")
}
session.alertMessage = "Read \(blocks.count) blocks."
session.invalidate()
}
}Getting System Info
tag.getSystemInfo(requestFlags: [.highDataRate, .address]) {
identifier, dsfid, afi, blockSize, blockCount, icReference, error in
guard error == nil else { return }
print("UID: \(identifier.map { String(format: "%02x", $0) }.joined())")
print("Block size: \(blockSize), Block count: \(blockCount)")
}MIFARE Tag Operations
func readMiFare(
tag: NFCMiFareTag,
session: NFCTagReaderSession
) {
// Identify MIFARE family
switch tag.mifareFamily {
case .ultralight:
readMiFareUltralight(tag: tag, session: session)
case .desfire:
session.invalidate(
errorMessage: "Use app-specific DESFire commands for this card."
)
case .plus:
print("MIFARE Plus tag detected")
session.invalidate()
case .unknown:
print("Unknown MIFARE tag")
session.invalidate()
@unknown default:
session.invalidate()
}
}
func readMiFareUltralight(
tag: NFCMiFareTag,
session: NFCTagReaderSession
) {
// READ command: reads 4 pages starting at page 4
let readCommand = Data([0x30, 0x04])
tag.sendMiFareCommand(commandPacket: readCommand) { data, error in
guard error == nil else {
session.invalidate(errorMessage: "Read failed.")
return
}
print("Read \(data.count) bytes from MIFARE Ultralight")
session.alertMessage = "Tag read successfully."
session.invalidate()
}
}FeliCa Tag Operations
FeliCa discovery requires .iso18092 polling and com.apple.developer.nfc.readersession.felica.systemcodes entries in Info.plist. Each system code must be explicit; wildcard values are not allowed.
func readFeliCa(
tag: NFCFeliCaTag,
session: NFCTagReaderSession
) {
tag.requestSystemCode { systemCodes, error in
guard error == nil else {
session.invalidate(errorMessage: "System code request failed.")
return
}
let codes = systemCodes.map {
$0.map { String(format: "%02x", $0) }.joined()
}
print("FeliCa system codes: \(codes.joined(separator: ", "))")
session.alertMessage = "FeliCa tag read successfully."
session.invalidate()
}
}Multi-Record NDEF Messages
Build messages with multiple records of different types:
func buildMultiRecordMessage() -> NFCNDEFMessage {
var records: [NFCNDEFPayload] = []
// Text record
if let textPayload = NFCNDEFPayload.wellKnownTypeTextPayload(
string: "Product: Widget Pro",
locale: Locale(identifier: "en")
) {
records.append(textPayload)
}
// URL record
if let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(
url: URL(string: "https://example.com/product/123")!
) {
records.append(urlPayload)
}
// Custom external type record
let externalPayload = NFCNDEFPayload(
format: .nfcExternal,
type: "com.example:product".data(using: .utf8)!,
identifier: Data(),
payload: """
{"sku":"WP-001","batch":"2026-03"}
""".data(using: .utf8)!
)
records.append(externalPayload)
return NFCNDEFMessage(records: records)
}Checking Message Size Against Tag Capacity
func writeIfFits(
message: NFCNDEFMessage,
to tag: any NFCNDEFTag,
session: NFCNDEFReaderSession
) {
tag.queryNDEFStatus { status, capacity, error in
guard status == .readWrite else {
session.invalidate(errorMessage: "Tag is not writable.")
return
}
let messageLength = message.length
guard messageLength <= capacity else {
session.invalidate(
errorMessage: "Message (\(messageLength) bytes) exceeds "
+ "tag capacity (\(capacity) bytes)."
)
return
}
tag.writeNDEF(message) { error in
if let error {
session.invalidate(
errorMessage: "Write failed: \(error.localizedDescription)"
)
} else {
session.alertMessage = "Written \(messageLength) bytes."
session.invalidate()
}
}
}
}NDEF Tag Locking
Lock a tag to make it permanently read-only. This is irreversible.
func lockTag(
_ tag: any NFCNDEFTag,
session: NFCNDEFReaderSession
) {
tag.queryNDEFStatus { status, _, error in
guard status == .readWrite else {
session.invalidate(errorMessage: "Tag is already read-only.")
return
}
tag.writeLock { error in
if let error {
session.invalidate(
errorMessage: "Lock failed: \(error.localizedDescription)"
)
} else {
session.alertMessage = "Tag locked permanently."
session.invalidate()
}
}
}
}SwiftUI NFC Scanner
Wrap the NFC reader in an @Observable model for SwiftUI integration:
import CoreNFC
import SwiftUI
@Observable
@MainActor
final class NFCScannerModel: NSObject {
var scannedText: String = ""
var scannedURL: URL?
var isScanning = false
var errorMessage: String?
private var session: NFCNDEFReaderSession?
var isAvailable: Bool {
NFCNDEFReaderSession.readingAvailable
}
func startScan() {
guard isAvailable else {
errorMessage = "NFC not available on this device."
return
}
session = NFCNDEFReaderSession(
delegate: self,
queue: nil,
invalidateAfterFirstRead: true
)
session?.alertMessage = "Hold your iPhone near an NFC tag."
session?.begin()
isScanning = true
errorMessage = nil
}
}
extension NFCScannerModel: NFCNDEFReaderSessionDelegate {
nonisolated func readerSessionDidBecomeActive(
_ session: NFCNDEFReaderSession
) { }
nonisolated func readerSession(
_ session: NFCNDEFReaderSession,
didDetectNDEFs messages: [NFCNDEFMessage]
) {
Task { @MainActor in
for message in messages {
for record in message.records {
if let url = record.wellKnownTypeURIPayload() {
scannedURL = url
}
if let (text, _) = record.wellKnownTypeTextPayload() {
scannedText = text
}
}
}
isScanning = false
}
}
nonisolated func readerSession(
_ session: NFCNDEFReaderSession,
didInvalidateWithError error: Error
) {
Task { @MainActor in
let nfcError = error as? NFCReaderError
if nfcError?.code != .readerSessionInvalidationErrorUserCanceled,
nfcError?.code != .readerSessionInvalidationErrorFirstNDEFTagRead {
errorMessage = error.localizedDescription
}
isScanning = false
self.session = nil
}
}
}
struct NFCScannerView: View {
@State private var scanner = NFCScannerModel()
var body: some View {
VStack {
if !scanner.isAvailable {
ContentUnavailableView(
"NFC Unavailable",
systemImage: "wave.3.right.circle.fill",
description: Text("This device does not support NFC.")
)
} else {
Button("Scan NFC Tag") {
scanner.startScan()
}
.buttonStyle(.borderedProminent)
.disabled(scanner.isScanning)
if let url = scanner.scannedURL {
Text("URL: \(url.absoluteString)")
}
if !scanner.scannedText.isEmpty {
Text("Text: \(scanner.scannedText)")
}
if let error = scanner.errorMessage {
Text(error).foregroundStyle(.red)
}
}
}
.padding()
}
}Error Handling Reference
NFCReaderError Codes
| Code | Meaning |
|---|---|
.readerSessionInvalidationErrorUserCanceled | User tapped Cancel in the NFC sheet |
.readerSessionInvalidationErrorFirstNDEFTagRead | Session ended after first read (when invalidateAfterFirstRead is true) |
.readerSessionInvalidationErrorSessionTimeout | 60-second session timeout elapsed |
.readerSessionInvalidationErrorSessionTerminatedUnexpectedly | System terminated the session |
.readerTransceiveErrorTagConnectionLost | Tag moved out of range during communication |
.readerTransceiveErrorRetryExceeded | Too many failed communication attempts |
.readerTransceiveErrorTagNotConnected | Attempted to communicate without connecting first |
.readerSessionInvalidationErrorSystemIsBusy | Another NFC session is active |
Graceful Error Recovery
nonisolated func readerSession(
_ session: NFCNDEFReaderSession,
didInvalidateWithError error: Error
) {
let nfcError = error as? NFCReaderError
Task { @MainActor in
switch nfcError?.code {
case .readerSessionInvalidationErrorUserCanceled:
break // User chose to cancel
case .readerSessionInvalidationErrorFirstNDEFTagRead:
break // Expected when invalidateAfterFirstRead is true
case .readerSessionInvalidationErrorSessionTimeout:
errorMessage = "Scan timed out. Try again."
case .readerTransceiveErrorTagConnectionLost:
errorMessage = "Tag moved away. Hold steady and try again."
default:
errorMessage = error.localizedDescription
}
self.session = nil
}
}Related skills
How it compares
Use core-nfc for Apple Core NFC entitlement and multi-format reader reviews rather than generic iOS skills without ISO 7816 or FeliCa specifics.
FAQ
Which session type should I use for URL tags?
Use NFCNDEFReaderSession for standard NDEF-formatted tags with URLs, text, or MIME payloads.
What entitlement format value should I add?
Add com.apple.developer.nfc.readersession.formats with the current TAG value; do not add legacy NDEF.
When is NFCTagReaderSession required?
Use it when you need ISO7816, ISO15693, FeliCa, or MIFARE protocol access beyond NDEF.
Is Core Nfc safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.