Scan and Dump

Use the top-level API when the app owns the NFC session. Keep the returned session alive while sending commands through the transport.

Scan

do {
    let (info, transport, session) = try await CoreExtendedNFC.scan()
    defer { session.invalidate() }
    // ...
} catch where error.isNFCCancellation {
    // The user dismissed the sheet (NFCError.userCanceled) or the task was cancelled.
}

When the user dismisses the NFC sheet, scan() throws NFCError.userCanceled, and later failures on that session's transport are reported the same way. Only one scan may be pending per NFCSessionManager; another call throws NFCError.scanInProgress. See Errors.

Polling Targets and Session Managers

scan and scanAndDump poll for every supported technology by default. Pass pollingTargets to narrow it, so other tag types in the field are not detected:

let (info, transport, session) = try await CoreExtendedNFC.scan(pollingTargets: [.iso15693])

Both also take a manager: argument. Pass your own NFCSessionManager, or a subclass that overrides hooks such as sessionDidBecomeActive(_:), sessionDidInvalidate(_:error:) or identifyTag(_:monitor:). scan returns the instance you passed; scanAndDump invalidates it before returning.

let manager = AppSessionManager()   // your NFCSessionManager subclass
let (info, dump) = try await CoreExtendedNFC.scanAndDump(manager: manager)

See Subclassing for the hooks and the thread-safety contract.

Identification Refinement

scan() first maps the CoreNFC tag family into a coarse CardType, then refines that result with family-specific probes where iOS exposes safe commands.

For MIFARE Type 2 tags, CoreNFC may only expose an unknown MIFARE family. If the coarse result looks like MIFARE Classic, CoreExtendedNFC validates the Type 2 manufacturer pages with READ 0x30 0x00 and the UID BCC bytes before retrying the Ultralight / NTAG GET_VERSION path. Real MIFARE Classic cards remain identification-only because iOS does not expose Crypto1 authentication.

Other refinements: Ultralight C is recognized by a 16-byte answer to READ 0x2B (no authentication is attempted; an Ultralight C with page 2B read-protected stays generic Ultralight). NTAG210 and NTAG212 are recognized from GET_VERSION. NTAG I2C (GET_VERSION product subtype 05h) shares storage-size codes with NTAG216 but not its memory map, so it stays generic Ultralight instead of being reported as NTAG216. Likewise Ultralight Nano and NTAG 223 DNA, whose GET_VERSION major version is not 01h, stay generic Ultralight; NTAG210u and NTAG213 TT get the NTAG210 / NTAG213 maps. readUltralightCAccessConfiguration decodes Ultralight C AUTH0 / AUTH1 per MF0ICU2 §7.5.8: AUTH0 30h or higher disables protection, and AUTH1 bit 0 = 1 restricts only writes (reads stay free) while 0 restricts reads and writes. FeliCa system code 88B4 is split into Lite and Lite-S by reading block 0x90, which only Lite-S has. ATQA 0304 with SAK 20 is DESFire with a random UID. Other SAK 20 (including MIFARE Plus SL3, which AN10833 §2.1 says is told apart by its ATS, not ATQA / SAK), 28 and 38 cards start as generic ISO 7816 (.smartMX) and are probed for DESFire, Type 4 and eMRTD.

Dump

let (info, dump) = try await CoreExtendedNFC.scanAndDump()
print(dump.summary.userSummary)

Some dumps are partial by design. Check dump.capabilities for .partiallyReadable and read dump.facts:

Cancelling the task stops a dump between pages, blocks or files with CancellationError.

Export

let json = try CoreExtendedNFC.exportJSON(dump: dump)
let flipper = try CoreExtendedNFC.exportFlipperNFC(dump: dump)
let artifacts = try CoreExtendedNFC.exportAll(dump: dump)

The Flipper, Proxmark3 MFU and libnfc MFD formats store exactly 4 bytes per Type 2 page. Their exports throw NFCError.malformedDump when any page has another length (for example a hand-edited or decoded dump) rather than padding or truncating it.