はじめに
WWDC 2019でiOS 13が発表され,様々な新機能が発表されました.初めはSwiftUIに話題が集中していましたが,徐々に落ち着き最近はBackgroundTasksやCore Haptics,CoreNFCなどの機能にも目線が行くようになりました.そんな中私の目に止まったのはCoreNFCの書き込み機能です.今までは限られた一部の形式の読み込みしかできなかったのがiOS 13から対応形式が増え,書き込みまでできるというのだから喜びました.
本記事ではCoreNFCのNFCNDEFReaderSession
を用いたNDEF形式のNFCタグへの読み書き方法を記します.←2019/8/26時点(iOS 13 beta 8&Xcode 11 beta 6)では書き込みが不完全っぽいです.←2019/9/12時点(iOS 13.1 beta 3&Xcode 11 GM seed)で成功しました🎉.
読み書き対象のNFCタグ形式
NDEFという形式のタグで,いわゆるFeliCaやMiFareと呼ばれる形式のタグのことです.
詳しくはこの記事を読んでみてください.
下準備
まず,TARGETS
のSigning & Capabilities
を開き,+を押してNear Field Communication Tag Readingを足しましょう.
.entitlementsを開いて項目が足されていることを確認します.
続いて,Info.plistを開いてPrivacy - NFC Scan Usage Description
を追加してなんかメッセージを追加しておきましょう.
ソース(抜粋)
GitHubに現状の物を上げてあります.
import UIKit
import CoreNFC
enum State {
case standBy
case read
case write
}
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var writeBtn: UIButton!
@IBOutlet weak var readBtn: UIButton!
var session: NFCNDEFReaderSession?
var message: NFCNDEFMessage?
var state: State = .standBy
var text: String = ""
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func tapScreen(_ sender: Any) {
textField.resignFirstResponder()
}
@IBAction func write(_ sender: Any) {
textField.resignFirstResponder()
if textField.text == nil || textField.text!.isEmpty { return }
text = textField.text!
startSession(state: .write)
}
@IBAction func read(_ sender: Any) {
startSession(state: .read)
}
func startSession(state: State) {
self.state = state
guard NFCNDEFReaderSession.readingAvailable else {
Swift.print("NFCはつかえないよ.")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session?.alertMessage = "NFCタグをiPhone上部に近づけてください."
session?.begin()
}
func stopSession(alert: String = "", error: String = "") {
session?.alertMessage = alert
if error.isEmpty {
session?.invalidate()
} else {
session?.invalidate(errorMessage: error)
}
self.state = .standBy
}
func tagRemovalDetect(_ tag: NFCNDEFTag) {
session?.connect(to: tag) { (error: Error?) in
if error != nil || !tag.isAvailable {
self.session?.restartPolling()
return
}
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + .milliseconds(500), execute: {
self.tagRemovalDetect(tag)
})
}
}
func updateMessage(_ message: NFCNDEFMessage) -> Bool {
if message.records.isEmpty { return false }
var results = [String]()
for record in message.records {
if let type = String(data: record.type, encoding: .utf8) {
if type == "T" { //データ形式がテキストならば
let res = record.wellKnownTypeTextPayload()
if let text = res.0 {
results.append("text: \(text)")
}
} else if type == "U" { //データ形式がURLならば
let res = record.wellKnownTypeURIPayload()
if let url = res {
results.append("url: \(url)")
}
}
}
}
stopSession(alert: "[" + results.joined(separator: ", ") + "]")
return true
}
}
extension ViewController: NFCNDEFReaderSessionDelegate {
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
//
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
Swift.print(error.localizedDescription)
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
// not called
}
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
if tags.count > 1 {
session.alertMessage = "読み込ませるNFCタグは1枚にしてください."
tagRemovalDetect(tags.first!)
return
}
let tag = tags.first!
session.connect(to: tag) { (error) in
if error != nil {
session.restartPolling()
return
}
}
tag.queryNDEFStatus { (status, capacity, error) in
if status == .notSupported {
self.stopSession(error: "このNFCタグは対応していないみたい.")
return
}
if self.state == .write {
if status == .readOnly {
self.stopSession(error: "このNFCタグには書き込みできないぞ")
return
}
if let payload = NFCNDEFPayload.wellKnownTypeTextPayload(string: self.text, locale: Locale(identifier: "en")) {
let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(string: "https://kyome.io/")!
self.message = NFCNDEFMessage(records: [payload, urlPayload])
if self.message!.length > capacity {
self.stopSession(error: "容量オーバーで書き込めないぞ!\n容量は\(capacity)bytesらしいぞ.")
return
}
tag.writeNDEF(self.message!) { (error) in
if error != nil {
// self.printTimestamp()
self.stopSession(error: error!.localizedDescription)
} else {
self.stopSession(alert: "書き込み成功\(^o^)/")
}
}
}
} else if self.state == .read {
tag.readNDEF { (message, error) in
if error != nil || message == nil {
self.stopSession(error: error!.localizedDescription)
return
}
if !self.updateMessage(message!) {
self.stopSession(error: "このNFCタグは対応していないみたい.")
}
}
}
}
}
func printTimestamp() {
let df = DateFormatter()
df.timeStyle = .long
df.dateStyle = .long
df.locale = Locale.current
let now = Date()
Swift.print("Timestamp: ", df.string(from: now))
}
}
注意点
読み込みをする際にNFCNDEFPayload.payload
がData
型だからと言って,``
func getMessage(_ message: NFCNDEFMessage) {
if message.records.isEmpty { return false }
for record in message.records {
guard let text = String(data: record.payload, encoding: String.Encoding.utf8) else {
continue
}
Swift.print(text)
}
}
という風にString(data: encoding: .utf8)
を使ってエンコードしようとしてもダメです.
これに気づき,wellKnownTypeTextPayloadを見つけるまでにひどく時間がかかってしまいました.
備考
CoreNFCの新機能は現在不完全で,Appleの実装スピードが遅い感じです.
iOS 13 beta 2の時点ではNDEFの機能が動いていたような気がするのですが,beta 4で完全に死に,beta 8までほっとかれる状態でした.(Release NoteをみてもCoreNFCの文字が全く見当たらないので不安です.)
現状おそらく実装の仕方は間違っていないのですが,MiFare形式で書き込みを試すと,
Ignored Exception: *** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
という解決不能なエラーが出てきてうまく書き込めません.
Appleさんにフィードバックを返しまくっているはずなのですが,何回フィードバックを返しても,ログをよこせという定型文を返してくるだけなのでどうすりゃいいのかよくわからない状態です(sysdiagnoseの送り方が間違っているのでしょうか).意思の疎通難しい.早く書き込みが安定するようにしてほしい.
現状CoreNFCのNDEFのRead/Writeに関する文献はほぼなく,Appleのサンプルコードも役に立たない(iOS13が更新されても更新されない)状況で方法を確立するのに大変時間がかかってしまいました.
この知見が皆様の役に立つことを願っています.