NFC (Near Field Communication) support for Skip apps on both iOS and Android.
SkipNFC provides a unified Swift API for NFC tag reading, writing, and communication.
On iOS it wraps Apple's CoreNFC framework. On Android, the Swift code is transpiled to
Kotlin and uses the android.nfc APIs.
Supported capabilities:
- NDEF message reading and writing on all tag types
- Tag type detection: ISO-DEP (ISO 14443-4), NFC-V (ISO 15693), NFC-F (FeliCa), and MIFARE Classic
- Tag UID access for identifying individual tags
- NDEF record creation for text, URI, and MIME type payloads
- NDEF record parsing with convenience accessors for text and URL content
- Raw transceive for sending low-level commands to tags
- Error handling with typed
NFCErrorcases - Polling options for selecting which NFC technologies to scan for
Add the dependency to your Package.swift file:
let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-nfc.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipNFC", package: "skip-nfc")
])
]
)Add android.permission.NFC to your AndroidManifest.xml:
<uses-permission android:name="android.permission.NFC" />Add the following to your entitlements and Info.plist:
- Near Field Communication Tag Reader Session Formats Entitlements
- NFCReaderUsageDescription
- com.apple.developer.nfc.readersession.iso7816.select-identifiers (if using ISO 7816 tags)
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
<key>NFCReaderUsageDescription</key>
<string>This app requires access to NFC to read and write data to NFC tags.</string>The simplest use case is scanning for NDEF messages. Create an NFCAdapter and call
startScanning with a message handler:
import SkipNFC
let adapter = NFCAdapter()
adapter.startScanning(messageHandler: { message in
for record in message.records {
print("Record type: \(record.typeName)")
if let text = record.textContent {
print("Text: \(text)")
}
if let url = record.urlContent {
print("URL: \(url)")
}
}
}, errorHandler: { error in
print("NFC error: \(error)")
})
// When done:
adapter.stopScanning()To interact with tags directly (read, write, or send commands), use the tagHandler:
let adapter = NFCAdapter(pollingOptions: [.iso14443, .iso15693])
adapter.alertMessage = "Hold your device near the NFC tag"
adapter.startScanning(tagHandler: { tag in
print("Tag UID: \(tag.identifier.map { String(format: "%02X", $0) }.joined(separator: ":"))")
Task {
do {
let message = try await tag.readMessage()
for record in message.records {
print("Record: \(record.textContent ?? "unknown")")
}
} catch {
print("Failed to read: \(error)")
}
}
})Create NDEF records and write them to a tag:
adapter.startScanning(tagHandler: { tag in
Task {
do {
let textRecord = NDEFRecord.makeTextRecord(text: "Hello from Skip!")
let uriRecord = NDEFRecord.makeURIRecord(url: "https://skip.dev")
let message = NDEFMessage.makeMessage(records: [textRecord, uriRecord])
try await tag.writeMessage(message)
print("Write successful")
} catch NFCError.tagReadOnly {
print("Tag is read-only")
} catch NFCError.tagNotNDEF {
print("Tag does not support NDEF")
} catch {
print("Write failed: \(error)")
}
}
})SkipNFC provides factory methods for creating common NDEF record types:
// Text record with language code
let text = NDEFRecord.makeTextRecord(text: "Bonjour", locale: "fr")
// URI record
let uri = NDEFRecord.makeURIRecord(url: "https://skip.dev")
// MIME type record with arbitrary data
let json = NDEFRecord.makeMIMERecord(type: "application/json", data: jsonData)
// Compose into a message
let message = NDEFMessage.makeMessage(records: [text, uri, json])Read the contents of NDEF records:
for record in message.records {
switch record.typeName {
case .nfcWellKnown:
// Text or URI record
if let text = record.textContent {
print("Text: \(text)")
} else if let url = record.urlContent {
print("URL: \(url)")
}
case .media:
// MIME type record
let mimeType = String(data: record.type, encoding: .utf8) ?? ""
print("MIME: \(mimeType), payload: \(record.payload.count) bytes")
default:
print("Other record type: \(record.typeName)")
}
}For advanced use cases, send raw commands to a tag using transceive:
adapter.startScanning(pollingOptions: [.iso14443], tagHandler: { tag in
guard let isoTag = tag as? NFCISODepTag else { return }
Task {
do {
// Send an APDU command
let command = Data([0x00, 0xA4, 0x04, 0x00])
let response = try await isoTag.transceive(data: command)
print("Response: \(response.map { String(format: "%02X", $0) }.joined())")
} catch {
print("Transceive failed: \(error)")
}
}
})import SwiftUI
import SkipNFC
struct NFCScannerView: View {
@State var adapter = NFCAdapter()
@State var scannedText: String = ""
@State var isScanning = false
var body: some View {
VStack(spacing: 16) {
Text(scannedText.isEmpty ? "Tap Scan to read an NFC tag" : scannedText)
.padding()
Button(isScanning ? "Stop" : "Scan") {
if isScanning {
adapter.stopScanning()
isScanning = false
} else {
adapter.alertMessage = "Hold your device near the NFC tag"
adapter.startScanning(messageHandler: { message in
for record in message.records {
if let text = record.textContent {
scannedText = text
} else if let url = record.urlContent {
scannedText = url.absoluteString
}
}
}, errorHandler: { error in
scannedText = "Error: \(error)"
})
isScanning = true
}
}
}
.padding()
}
}The main interface for NFC scanning.
| Property / Method | Description |
|---|---|
init(pollingOptions:) |
Create an adapter, optionally specifying which NFC technologies to scan for |
isAvailable: Bool |
Whether NFC hardware is available on this device |
isReady: Bool |
Whether NFC is enabled and ready for use |
alertMessage: String? |
The iOS prompt message shown during scanning |
startScanning(messageHandler:tagHandler:errorHandler:) |
Begin scanning for NFC tags |
stopScanning() |
Stop scanning |
| Option | Description |
|---|---|
.iso14443 |
ISO/IEC 14443 Type A/B (IsoDep, NfcA, NfcB) |
.iso15693 |
ISO/IEC 15693 (NfcV) |
.iso18092 |
NFC-F / FeliCa |
.pace |
PACE (iOS only) |
| Property / Method | Description |
|---|---|
makeMessage(records:) |
Create a message from an array of records |
records: [NDEFRecord] |
The records in this message |
| Property / Method | Description |
|---|---|
makeTextRecord(text:locale:) |
Create a well-known text record |
makeURIRecord(url:) |
Create a well-known URI record |
makeMIMERecord(type:data:) |
Create a MIME type record |
identifier: Data |
Record identifier |
type: Data |
Record type |
payload: Data |
Raw payload data |
typeName: TypeName |
The type name format (.nfcWellKnown, .media, etc.) |
textContent: String? |
Parse payload as text (nil if not a text record) |
urlContent: URL? |
Parse payload as URL (nil if not a URI record) |
All tag types conform to NFCTagImpl and provide:
| Property / Method | Description |
|---|---|
identifier: Data |
The tag's unique identifier (UID) |
readMessage() async throws |
Read the NDEF message from the tag |
writeMessage(_:) async throws |
Write an NDEF message to the tag |
transceive(data:) async throws |
Send a raw command and receive a response |
| Tag Class | NFC Technology | iOS Type | Android Type |
|---|---|---|---|
NFCISODepTag |
ISO-DEP (ISO 14443-4) | NFCISO7816Tag |
IsoDep |
NFCVTag |
NFC-V (ISO 15693) | NFCISO15693Tag |
NfcV |
NFCFTag |
NFC-F (FeliCa) | NFCFeliCaTag |
NfcF |
NFCMTag |
MIFARE Classic | NFCMiFareTag |
MifareClassic |
NFCISODepTag also provides historicalBytes: Data? from the tag's answer-to-select response.
| Case | Description |
|---|---|
.notAvailable |
NFC hardware is not available |
.tagNotNDEF |
Tag does not support NDEF |
.tagReadOnly |
Tag is read-only |
.readFailed(String) |
Read operation failed |
.writeFailed(String) |
Write operation failed |
.connectionFailed(String) |
Connection to tag failed |
.transceiveFailed(String) |
Raw command failed |
.sessionError(String) |
Session or system error |
This project is a free Swift Package Manager module that uses the Skip plugin to transpile Swift into Kotlin.
Building the module requires that Skip be installed using
Homebrew with brew install skiptools/skip/skip.
This will also install the necessary build prerequisites:
Kotlin, Gradle, and the Android build tools.
The module can be tested using the standard swift test command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with skip test,
which will output a table of the test results for both platforms.
This software is licensed under the Mozilla Public License 2.0.