From apple-kit-skills
Reads and writes NFC tags on iOS using CoreNFC. Use for NDEF scanning, ISO7816/ISO15693/FeliCa/MIFARE tag sessions, NDEF message writing, and background tag reading.
How this skill is triggered — by the user, by Claude, or both
Slash command
/apple-kit-skills:core-nfcThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Read and write NFC tags on iPhone using the CoreNFC framework. Covers NDEF
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+.
NFCReaderUsageDescription to Info.plist with a user-facing reason stringcom.apple.developer.nfc.readersession.formats entitlement with the current TAG value; do not add legacy NDEFcom.apple.developer.nfc.readersession.iso7816.select-identifiers in Info.plistcom.apple.developer.nfc.readersession.felica.systemcodes; do not use wildcard system codesNFC 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
}
| 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 |
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
}
}
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()
}
}
}
}
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
}
}
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()
}
}
}
}
// 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)!
)
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
}
}
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)
}
}
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.
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.
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
}
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()
}
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
}
}
NFCReaderUsageDescription set in Info.plistcom.apple.developer.nfc.readersession.formats entitlement uses TAG, not legacy NDEFNFCNDEFReaderSession.readingAvailable or NFCTagReaderSession.readingAvailable checked before creating sessionsbegin()didInvalidateWithError distinguishes user cancellation from actual errorsNFCTagReaderSession.iso18092NFCTagReaderSessionnpx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsProvides CryptoTokenKit guidance for building TKTokenDriver/TKSmartCardTokenDriver extensions, communicating with smart cards via APDU, integrating token-backed keychain items, and configuring certificate-based auth on Apple platforms.
Provides Apple HIG principles for technologies like Siri, Apple Pay, HealthKit, ARKit, Core ML, Sign in with Apple, ensuring privacy, accessibility, and safety in iOS/macOS apps.
Builds Salesforce LWCs that use native mobile device capabilities: barcode scanner, biometrics, location, NFC, calendar, contacts, document scanner, geofencing, AR space capture, app review, and payments.