From apple-kit-skills
Read and write NFC tags on iOS using CoreNFC: NDEF/tag reader sessions, ISO7816/15693/FeliCa/MIFARE support, message writing, entitlements, background scanning.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsThis skill uses the workspace's default tool permissions.
Read and write NFC tags on iPhone using the CoreNFC framework. Covers NDEF
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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 tag types your app reads (e.g., NDEF, TAG)com.apple.developer.nfc.readersession.iso7816.select-identifiers in Info.plistNFC 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
}
| 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).
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
}
}
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. To opt in:
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)
}
}
}
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
)
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()
}
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 configured with correct tag typesNFCNDEFReaderSession.readingAvailable checked before creating sessionsbegin()didInvalidateWithError distinguishes user cancellation from actual errorsNFCTagReaderSession