iTranslated by AI
Reading and Writing NDEF with Core NFC (Swift)
Introduction
It seems like writing is also possible with Core NFC starting from iOS 13, so I tried it out.
(I'm using UIKit instead of SwiftUI since it involves delegates and such.)
Click here for the SwiftUI version.
The finished product looks like this.
I'm writing the entered text and a fixed URL.
Full Source Code
import UIKit
import CoreNFC
final class ViewController: UIViewController {
@IBOutlet private weak var textField: UITextField!
private var session: NFCNDEFReaderSession!
/// Write mode flag
private var isWriting = false
private var ndefMessage: NFCNDEFMessage!
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
}
@IBAction private func read(_ sender: Any) {
isWriting = false
startSession()
}
@IBAction private func write(_ sender: Any) {
isWriting = true
let textPayload = NFCNDEFPayload(
format: NFCTypeNameFormat.nfcWellKnown,
type: "T".data(using: .utf8)!,
identifier: Data(),
payload: textField.text!.data(using: .utf8)!
)
let uriPayload = NFCNDEFPayload.wellKnownTypeURIPayload(url: .init(string: "https://www.am10.blog/")!)
ndefMessage = NFCNDEFMessage(records: [textPayload, uriPayload!])
startSession()
}
private func startSession() {
guard NFCNDEFReaderSession.readingAvailable else {
showAlert(message: "デバイス対応してないよ( ̄∀ ̄)")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session.alertMessage = "スキャン中"
session.begin()
}
}
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
extension ViewController: NFCNDEFReaderSessionDelegate {
// Required
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// Required
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// Optional, but something shows up in the console
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
let tag = tags.first!
session.connect(to: tag) { error in
tag.queryNDEFStatus() { [unowned self] status, capacity, error in
if self.isWriting {
// Write
if status == .readWrite {
self.write(tag: tag, session: session)
return
}
} else {
// Read
if status == .readOnly || status == .readWrite {
self.read(tag: tag, session: session)
return
}
}
session.invalidate(errorMessage: "タグがおかしいよ(´∇`)")
}
}
}
private func write(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.writeNDEF(self.ndefMessage) { error in
session.alertMessage = "書き込み完了♬(ノ゜∇゜)ノ♩"
session.invalidate()
}
}
private func read(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.readNDEF { [unowned self] message, error in
session.alertMessage = "読み込み完了♬(ノ゜∇゜)ノ♩"
session.invalidate()
let text = message?.records.compactMap {
switch $0.typeNameFormat {
case .nfcWellKnown:
if let url = $0.wellKnownTypeURIPayload() {
return url.absoluteString
}
if let text = String(data: $0.payload, encoding: .utf8) {
return text
}
return nil
default:
return nil
}
}.joined(separator: "\n\n")
DispatchQueue.main.async {
self.showAlert(message: text ?? "何も取れなかったよ(゚∀゚)")
}
}
}
}
extension UIViewController {
func showAlert(message: String) {
let alert = UIAlertController(title: "", message: message, preferredStyle: .alert)
alert.addAction(.init(title: "OK", style: .default) { _ in } )
present(alert, animated: true)
}
}
Environment
- Xcode 15
- iOS 17.0.1 (iPhone 12 mini)
- NFC Tag
Preparation
Add "Near Field Communication Tag Reading" from Signing & Capabilities.
Add "Privacy - NFC Scan Usage Description" to Info.plist.
Place a text field and buttons on the Storyboard and connect them.
Reading
The reading process can be implemented as follows.
private var session: NFCNDEFReaderSession!
@IBAction private func read(_ sender: Any) {
startSession()
}
private func startSession() {
guard NFCNDEFReaderSession.readingAvailable else {
print("デバイス対応してないよ( ̄∀ ̄)")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session.alertMessage = "スキャン中"
session.begin()
}
extension ViewController: NFCNDEFReaderSessionDelegate {
// Required
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
session.alertMessage = "読み込み完了♬(ノ゜∇゜)ノ♩"
session.invalidate()
let text = messages.first?.records.compactMap {
switch $0.typeNameFormat {
case .nfcWellKnown:
if let url = $0.wellKnownTypeURIPayload() {
return url.absoluteString
}
let payload = $0.wellKnownTypeTextPayload()
if let text = payload.0, let locale = payload.1 {
return "\(text)\n\(locale)"
}
return nil
default:
return nil
}
}.joined(separator: "\n\n")
print(text ?? "何も取れなかったよ(゚∀゚)")
}
// Required
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// Optional, but something shows up in the console
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
}
}
There seem to be various NFCTypeNameFormat options as shown below, but this time I will only use nfcWellKnown (since it allows retrieving text and URLs).
public enum NFCTypeNameFormat : UInt8, @unchecked Sendable {
@available(iOS 11.0, *)
case empty = 0
@available(iOS 11.0, *)
case nfcWellKnown = 1
@available(iOS 11.0, *)
case media = 2
@available(iOS 11.0, *)
case absoluteURI = 3
@available(iOS 11.0, *)
case nfcExternal = 4
@available(iOS 11.0, *)
case unknown = 5
@available(iOS 11.0, *)
case unchanged = 6
}
A Little Improvement
While reading is possible with the implementation above, I will modify it slightly to support writing.
To handle the writing process, you need to implement the following delegate method:
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag])
When the method above is implemented, the following method that was used for reading will no longer be called:
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage])
Important
The reader session doesn’t call this method when the delegate provides the readerSession(_:didDetect:) method.
Reference: readerSession(_:didDetectNDEFs:)
I will modify the code as follows to support writing as well:
extension ViewController: NFCNDEFReaderSessionDelegate {
// Required (no longer called if didDetect is implemented)
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// Required
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// Optional, but something appears in the console
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
let tag = tags.first!
session.connect(to: tag) { error in
tag.queryNDEFStatus() { [unowned self] status, capacity, error in
// Reading
if status == .readOnly || status == .readWrite {
self.read(tag: tag, session: session)
return
}
session.invalidate(errorMessage: "タグがおかしいよ(´∇`)")
}
}
}
private func read(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.readNDEF { [unowned self] message, error in
session.alertMessage = "読み込み完了♬(ノ゜∇゜)ノ♩"
session.invalidate()
let text = message?.records.compactMap {
switch $0.typeNameFormat {
case .nfcWellKnown:
if let url = $0.wellKnownTypeURIPayload() {
return url.absoluteString
}
let payload = $0.wellKnownTypeTextPayload()
if let text = payload.0, let locale = payload.1 {
return "\(text)\n\(locale)"
}
return nil
default:
return nil
}
}.joined(separator: "\n\n")
print(text ?? "何も取れなかったよ(゚∀゚)")
}
}
}
This completes the reading process.
Writing
The writing process can be done as follows.
@IBOutlet private weak var textField: UITextField!
private var ndefMessage: NFCNDEFMessage!
private var session: NFCNDEFReaderSession!
private func startSession() {
guard NFCNDEFReaderSession.readingAvailable else {
showAlert(message: "Device not supported( ̄∀ ̄)")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session.alertMessage = "Scanning"
session.begin()
}
@IBAction private func write(_ sender: Any) {
let textPayload = NFCNDEFPayload.wellKnownTypeTextPayload(
string: textField.text!,
locale: Locale(identifier: "ja_JP")
)
let uriPayload = NFCNDEFPayload.wellKnownTypeURIPayload(url: .init(string: "https://www.am10.blog/")!)
ndefMessage = NFCNDEFMessage(records: [textPayload!, uriPayload!])
startSession()
}
extension ViewController: NFCNDEFReaderSessionDelegate {
// Required
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// Required
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// Optional, but something shows up in the console
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
let tag = tags.first!
session.connect(to: tag) { error in
tag.queryNDEFStatus() { [unowned self] status, capacity, error in
if status == .readWrite {
self.write(tag: tag, session: session)
return
}
session.invalidate(errorMessage: "Something is wrong with the tag(´∇`)")
}
}
}
private func write(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.writeNDEF(self.ndefMessage) { error in
session.alertMessage = "Writing complete♬(ノ゜∇゜)ノ♩"
session.invalidate()
}
}
}
You just need to set the values in NFCNDEFMessage and write using tag.writeNDEF(:).
The reason for having the following in the ViewController is that accessing the UITextField within the delegate method triggered a warning that it wasn't on the main thread, so I'm generating it at the time the button is pressed.
private var ndefMessage: NFCNDEFMessage!
A Slight Improvement in Reading and Writing Text
I was able to read and write without any problems as described above, but when I tried reading the written text data with other apps, the characters were garbled (the URL was displayed correctly).
I improved the text reading/writing process as follows so that it can be read by other apps as well.
// Reading process
// (Modified the process in private func read(tag: NFCNDEFTag, session: NFCNDEFReaderSession))
case .nfcWellKnown:
if let text = String(data: $0.payload, encoding: .utf8) {
return text
}
return nil
// Writing process
// (Modified the process in @IBAction private func write(_ sender: Any))
let textPayload = NFCNDEFPayload(
format: NFCTypeNameFormat.nfcWellKnown,
type: "T".data(using: .utf8)!,
identifier: Data(),
payload: textField.text!.data(using: .utf8)!
)
This allows other apps to read the text successfully.
Conclusion
Since I have barely implemented any error handling this time, you will likely need to add such processes when actually creating an app.
Anyway, you can now freely read and write text and URLs to NFC tags!
Discussion