CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

nfc-patterns.mdskills/core-nfc/references/

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
  • 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

CommandCLAINSDescription
SELECT0x000xA4Select an application or file
READ BINARY0x000xB0Read data from a transparent file
UPDATE BINARY0x000xD6Write data to a transparent file
READ RECORD0x000xB2Read a record from a record-oriented file
GET DATA0x000xCARetrieve 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:
        readMiFareDESFire(tag: tag, session: session)
    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()
    }
}

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

CodeMeaning
.readerSessionInvalidationErrorUserCanceledUser tapped Cancel in the NFC sheet
.readerSessionInvalidationErrorFirstNDEFTagReadSession ended after first read (when invalidateAfterFirstRead is true)
.readerSessionInvalidationErrorSessionTimeout60-second session timeout elapsed
.readerSessionInvalidationErrorSessionTerminatedUnexpectedlySystem terminated the session
.readerTransceiveErrorTagConnectionLostTag moved out of range during communication
.readerTransceiveErrorRetryExceededToo many failed communication attempts
.readerTransceiveErrorTagNotConnectedAttempted to communicate without connecting first
.readerSessionInvalidationErrorSystemIsBusyAnother 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
    }
}

skills

core-nfc

CHANGELOG.md

README.md

tile.json