Swift-ios-skills 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 lifecycle, configuring NFC entitlements, or implementing background tag reading in iOS apps.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/core-nfc" ~/.claude/skills/dpearson2699-swift-ios-skills-core-nfc && rm -rf "$T"
skills/core-nfc/SKILL.mdCoreNFC
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
- Add the Near Field Communication Tag Reading capability in Xcode
- Add
to Info.plist with a user-facing reason stringNFCReaderUsageDescription - Add the
entitlement with the tag types your app reads (e.g.,com.apple.developer.nfc.readersession.formats
,NDEF
)TAG - For ISO 7816 tags, add supported application identifiers to
in Info.plistcom.apple.developer.nfc.readersession.iso7816.select-identifiers
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 |
|---|---|
| Scans for NDEF-formatted tags |
| Scans for ISO7816, ISO15693, FeliCa, MIFARE tags |
| Collection of NDEF payload records |
| Single record within an NDEF message |
| 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
-
set in Info.plistNFCReaderUsageDescription -
entitlement configured with correct tag typescom.apple.developer.nfc.readersession.formats -
checked before creating sessionsNFCNDEFReaderSession.readingAvailable - Session delegate set before calling
begin() - Session reference set to nil after invalidation
-
distinguishes user cancellation from actual errorsdidInvalidateWithError - 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
- 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