🕙

Core NFCでNDEFを読み書き(SwiftUI版)

2023/09/26に公開

はじめに

前回はデリゲートがめんどくさそうだったので UIKit でやったのですが SwiftUI でも簡単にできそうだったので SwiftUI でやってみました。

完成品はこんな感じ。
入力したテキストと固定値の URL を書き込んでいます。

nfc_swift_ui

ソース

import Foundation
import CoreNFC
import SwiftUI

final class NFCSession: NSObject, ObservableObject {

    private var session: NFCNDEFReaderSession!
    private var isWriting = false
    private var ndefMessage: NFCNDEFMessage!
    private var writeHandler: ((Error?) -> Void)?
    private var readHandler: ((String?, Error?) -> Void)?

    func startWriteSession(text: String, writeHandler: ((Error?) -> Void)?) {
        self.writeHandler = writeHandler
        isWriting = true
        let textPayload = NFCNDEFPayload(
            format: NFCTypeNameFormat.nfcWellKnown,
            type: "T".data(using: .utf8)!,
            identifier: Data(),
            payload: text.data(using: .utf8)!)
        let uriPayload = NFCNDEFPayload.wellKnownTypeURIPayload(url: .init(string: "https://www.am10.blog/")!)
        ndefMessage = NFCNDEFMessage(records: [textPayload, uriPayload!])
        startSession()
    }

    func startReadSession(readHandler: ((String?, Error?) -> Void)?) {
        self.readHandler = readHandler
        isWriting = false
        startSession()
    }

    private func startSession() {
        guard NFCNDEFReaderSession.readingAvailable else {
            return
        }
        session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
        session.alertMessage = "スキャン中"
        session.begin()
    }
}

extension NFCSession: 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) { [unowned self] error in
            session.alertMessage = "書き込み完了♬(ノ゜∇゜)ノ♩"
            session.invalidate()
            DispatchQueue.main.async {
                self.writeHandler?(error)
            }
        }
    }

    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.readHandler?(text, error)
            }
        }
    }
}

struct ContentView: View {
    @State private var isAlertShown = false
    @State private var alertMessage = ""
    @State private var text = ""
    @StateObject private var session = NFCSession()
    var body: some View {
        VStack(spacing: 16) {
            TextField("何か入力", text: $text)

            Button {
                session.startReadSession { text, error in
                    if let error = error {
                        alertMessage = error.localizedDescription
                    } else {
                        alertMessage = text ?? "空だよ"
                    }
                    isAlertShown = true
                }
            } label: {
                Text("Read")
            }

            Button {
                session.startWriteSession(text: text) { error in
                    if let error = error {
                        alertMessage = error.localizedDescription
                        isAlertShown = true
                    }
                }
            } label: {
                Text("Write")
            }
        }
        .padding(16)
        .alert(isPresented: $isAlertShown) {
            Alert(
                title: Text(""),
                message: Text(alertMessage),
                dismissButton: .default(Text("OK")))
        }
    }
}

環境と事前準備

環境と事前準備は Storyboard のとこ以外は前回と同じです。

実装

ほとんど実装は UIKit 版と同じですがポイントは NFCSession です。

下記2つのクロージャを持たせて NFCNDEFReaderSessionDelegate で値を受けたあとの処理を実行しています(たぶんメモリリークはしてないはず)。

private var writeHandler: ((Error?) -> Void)?
private var readHandler: ((String?, Error?) -> Void)?

今回もエラーハンドリングをあまりしていないのでアプリ作成時は適宜実装してください🙇‍♂️

おわりに

これで SwiftUI でも Core NFC でテキストと URL の読み書きができるようになりました🙌

読み書きできてもあまり用途が思いつかないですがとりあえずワンタップで任意の URL を書き込めるアプリを作ってみました。

nfc_app

NFC タグを持ち歩いていればどこかで使えるかも?

Discussion