Core NFCでNDEFを読み書き(Swift)
はじめに
iOS 13 から Core NFC で書き込みもできるらしいのでやってみました。
(デリゲートとかあるので SwiftUI ではなく UIKit でやってます。)
SwiftUI 版はこちら。
完成品はこんな感じ。
入力したテキストと固定値の URL を書き込んでいます。
ソース全体
import UIKit
import CoreNFC
final class ViewController: UIViewController {
@IBOutlet private weak var textField: UITextField!
private var session: NFCNDEFReaderSession!
/// 書き込みモードフラグ
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 {
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// 必須ではないけどコンソールになんかでる
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 {
// 書き込み
if status == .readWrite {
self.write(tag: tag, session: session)
return
}
} else {
// 読み込み
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)
}
}
環境
- Xcode 15
- iOS 17.0.1(iPhone 12 mini)
- NFC タグ
事前準備
Signing & Capabilities から「Near Field Communication Tag Reading」を追加。
Info.plist に「Privacy - NFC Scan Usage Description」を追加。
Storyboard にテキストフィールドとボタンを置いて紐づけておく。
読み取り
読み取り処理は下記でできます。
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 {
// 必須
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 ?? "何も取れなかったよ(゚∀゚)")
}
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// 必須ではないけどコンソールになんかでる
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
}
}
NFCTypeNameFormat
は下記のように色々あるみたいですが今回は nfcWellKnown
のみ使います(これでテキストと URL がとれたので)。
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
}
ちょっと改善
読み取り処理は上記でも可能なのです書き込みに対応するため少し修正します。
書き込み処理のためには下記のデリゲートを実装する必要があります。
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag])
上記メソッドを実装すると読み込み処理で利用していた下記メソッドが呼ばれなくなってしまいます。
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage])
Important
The reader session doesn’t call this method when the delegate provides the readerSession(_:didDetect:) method.
参考:readerSession(_:didDetectNDEFs:)
書き込みにも対応するために下記のように修正します。
extension ViewController: NFCNDEFReaderSessionDelegate {
// 必須(didDetect を実装すると呼ばれなくなる)
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// 必須ではないけどコンソールになんかでる
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 == .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 ?? "何も取れなかったよ(゚∀゚)")
}
}
}
これで読み取りは完了です。
書き込み
読み取り処理は下記でできます。
@IBOutlet private weak var textField: UITextField!
private var ndefMessage: NFCNDEFMessage!
private var session: NFCNDEFReaderSession!
private func startSession() {
guard NFCNDEFReaderSession.readingAvailable else {
showAlert(message: "デバイス対応してないよ( ̄∀ ̄)")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session.alertMessage = "スキャン中"
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 {
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
}
// 必須
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
}
// 必須ではないけどコンソールになんかでる
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: "タグがおかしいよ(´∇`)")
}
}
}
private func write(tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
tag.writeNDEF(self.ndefMessage) { error in
session.alertMessage = "書き込み完了♬(ノ゜∇゜)ノ♩"
session.invalidate()
}
}
}
NFCNDEFMessage
に値を設定して tag.writeNDEF(:)
で書き込むだけです。
下記を ViewController
に持たせているのはデリゲートメソッド内で UITextField
にアクセスするとメインスレッドではないと警告が出たためボタン押下時に生成処理をしています。
private var ndefMessage: NFCNDEFMessage!
テキストの読み書きちょっと改善
上記で問題なく読み書きできたのですが書き込んだテキストデータを他のアプリで読み込んでみたところ文字化けしてしまいました(URL は表示されました)。
他のアプリでも読み込めるようにテキストの読み書き処理を下記のように改善しました。
// 読み込み処理
// (private func read(tag: NFCNDEFTag, session: NFCNDEFReaderSession)の処理を修正)
case .nfcWellKnown:
if let text = String(data: $0.payload, encoding: .utf8) {
return text
}
return nil
// 書き込み処理
// (@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)!
)
これで他のアプリでもテキストを読み込めるようになりました。
おわりに
今回はほとんどエラーハンドリングをしていないので実際にアプリを作成する際はそのあたりの処理も追加する必要があると思います。
とりあえずはこれで NFC タグに自由にテキストと URL を読み書きできるようになりました!
Discussion