iPhone X/XSでNFCタグ（RFIDタグ）を使う

# はじめに
RFIDタグってご存知でしょうか？
iPhoneやAndroid界隈ではNFCタグと言った方が分かりやすいかもしれません。
一番有名なのは、「Suica」[^Suica]や「楽天Edy」[^楽天Edy]でしょうか。駅の改札やコンビニでピッとやるあれです。

RFIDタグは非常に安く製造できるために、上記のような決済だけで無く、店舗の在庫管理や、図書館での蔵書管理など物品管理を始めとして様々なビジネスで活用されています。

# NFCタグ（RFIDタグ）とは

それぞれ下記の略称です。

RFID：Radio Frequency Identifier
NFC：Near Field Communication

RFIDは
１）比較的離れた場所から、複数のタグを一括して識別することが可能な電磁波タイプ
２）近接して読み取るために省エネルギーで指向性が高い電磁誘導タイプ
があります。
NFCはRFIDの一種で後者の電磁誘導タイプになります。

iPhoneでは、iOS11.0から導入された[CoreNFCフレームワーク](https://developer.apple.com/documentation/corenfc)でNFCタグに対応しました。
しかしながらiPhoneにNFCのハードウェアが必要なために、iPhone8より古い端末では動作することができません。

iPhone Xまでは、アプリを起動しなくてはNFCタグを読むことができませんでしたが、iPhone XSからはアプリを起動しないでNFCタグを読み込むことができるようになり、大変便利になりました。この手法については[#バックグラウント読み取り](https://qiita.com/BlueEventHorizon/items/f17da2ea0cee72214c11#バックグラウント読み取り)で取り上げます。

# なぜNFCを使うのか

[私の所属する会社](https://www.yumemi.co.jp/ja)は、百数十名を超えるメンバーがおり、また現在オフィスのフリーアドレス化を進めています。そのため、メンバーの「勤怠状況」や「どこに誰がいるのか」をある程度把握する必要がありました。この課題に対して社員の方から様々な提案がある中で、私も何か方法はないものかと考え、NFCを使って勤怠と位置を簡単に共有するというアイデアにたどり着きました。

アイデアの概要は以下のようになります。

・出社（再開）、中断、終了など数種類の情報をNFCタグに埋め込み、すべての机に前述のセットで貼り付ける。
・また出社（再開）タグには、例えば6F-A3（６階のA3番の机）のような机を識別するIDも埋め込みます。
・出社時、中断時、再開時、終了時にこのタグをiPhoneでスキャンすることで、上記の勤怠状況と位置情報、さらにユーザ識別情報をサーバにアップロードします。
・例えばオフィス内のマップにユーザを表示すれば、誰がどこに出社しているか一目でわかります。

![office_x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/61e3c6c0-e277-ceaf-47bb-47f8ad6e7d45.png)

表示すべき内容は今後詳細を詰めていきます。

# NFCタグを入手

まずNFCタグが必要です。また数百枚のタグを購入するかもしれないので、価格も気になります。
これはAmazonで調べると容易に入手できることが分かりました。

[サンワサプライ NFCタグ(10枚入り) 白 MM-NFCT](https://www.amazon.co.jp/サンワサプライ-NFCタグ-10枚入り-白-MM-NFCT/dp/B00GXSGL5G)
10枚入りで972円です。ついでに [オリジナルアプリ「NFCかんたん設定アプリ」](https://www.sanwa.co.jp/seihin_joho/nfc/index.html) も付いています。

<font color="DeepPink">実はiOSは、2019年7月時点ではNFCタグに情報を書き込むことができません。</font>これができるのは、iOS13を待たなくてはなりません。そこで今回は上記のオリジナルアプリを動かすために、Android端末を借りてきました。

# バックグラウント読み取り

最初に 

> iPhone XSからはアプリを起動しないでNFCタグを読み込むことができるようになり

と書きましたが、何もしなければiPhone XSで起動できるのは下記のようなもののみです。

- URLを記載したタグで`Safari`アプリを起動
- 電子メールアドレスを記載したタグで`メール`アプリを起動
- 電話番号を記載したタグで`電話`アプリを起動

自分のアプリを起動することはできませんので、`Universal Links`を使うことにします。
`Universal Links`を使うことで、所定のURLにクエリーを追加することで

>・出社（再開）、中断、終了など数種類の情報をNFCタグに埋め込み、すべての机に前述のセットで貼り付ける。
・また出社（再開）タグには、例えば6F-A3（６階のA3番の机）のような机を識別するIDも埋め込みます。

が可能になります。

また、Appleのドキュメント[「Adding Support for Background Tag Reading」](https://developer.apple.com/documentation/corenfc/adding_support_for_background_tag_reading) によるとバックグラウンドで読み取るためには以下のような制限がありますので注意が必要です。

- iPhoneのディスプレイがオンの状態（ロックされていても良い）
- 再起動後に少なくとも1回ロック解除されていること
- Core NFC読み取りセッションが動作中でないこと
- Apple Payが使用中でないこと
- カメラが起動中でないこと
- 機内モードがオフであること


# Xcode：プロジェクトの設定

まずinfo.plistにプライバシーポリシーの記述が必要です。

<font color="Blue"> NFCReaderUsageDescription </font>
[ドキュメントはこちら](https://developer.apple.com/documentation/bundleresources/information_property_list/nfcreaderusagedescription)
![スクリーンショット 2019-07-28 23.59.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/340d8da0-f16e-476e-ea38-c77e487d7c23.png)


また、targetのcapabilitiesで、NFC、Universal Linksの有効化が必要です。
これらをオンにすることで、entitlementsファイルが自動的に生成されプロジェクトに追加されます。

<font color="Blue"> NFC有効化 </font>
![nfc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/ea77085b-60c0-2c32-4376-7875ce2a5b4c.png)

<font color="Blue"> Universal Links有効化 </font>
![ulinks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/fb0ba8e1-2a18-91db-2fc3-16de0f4c2612.png)

# Xcode：実装

NFC読み取りのコード自体は非常にシンプルです。
[Building an NFC Tag-Reader App](https://developer.apple.com/documentation/corenfc/building_an_nfc_tag-reader_app)
Appleにサンプルソースコードは上記のURLから入手できるのですが、ここではUIViewControllerに数行追加するだけで基本的な動作をするコードを紹介します。

まずNFCUtilクラスです。このクラスはNFCNDEFReaderSessionプロパティを保持し、startメソッドで読み取りを開始、DIされたURLRouterを結果を外部に通知します。

```swift:NFCUtil
import CoreNFC

@available(iOS 11.0, *)
public class NFCUtil: NSObject {
    private var session: NFCNDEFReaderSession?
    private var router: URLRouter

    public func start() {
        session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
        session?.alertMessage = "タグを読み取りますので、iPhoneをタグの近くに近づけて、しばらくそのままでお待ちください。"
        session?.begin()
    }

    required public init(_ router: URLRouter) {
        self.router = router
        super.init()
    }
}
```

URLRouterは下記のようなprotocolになっています。

```swift:URLRouter
public protocol URLRouter {
    func exec(query: URL) -> Bool
}
```

NFCタグの読み取り結果が返ってくる`NFCNDEFReaderSessionDelegate`にURLRouterを実行するコードを記述します。正常終了かエラーのメソッドしかないのですごく簡単です。

```swift:Extension
@available(iOS 11.0, *)
extension NFCUtil: NFCNDEFReaderSessionDelegate {

    // 正常終了
    public func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
        DispatchQueue.main.async {
            // NFCNDEFMessageからURLを取り出す
            for message in messages {
                for record in message.records {
                    var s = String(data: record.payload, encoding: .utf8)!
                    // サンワサプライ NFCタグの特別対応？
                    // 先頭に\0が埋め込まれているので、その場合は除去する
                    if String(s.prefix(1)) == "\0" {
                        s = String(s.suffix(s.count-1))
                    }
                    logger.info("content = \(s)")
                    if let _url = URL(string: s) {
                        // routerで取得したURLを実行する
                        _ = self.router.exec(query: _url)
                    }
                }
            }
        }
    }

    /// エラー
    public func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
        // Check the invalidation reason from the returned error.
        if let _error = error as? NFCReaderError {
            if (_error.code != .readerSessionInvalidationErrorFirstNDEFTagRead) && (_error.code != .readerSessionInvalidationErrorUserCanceled) {
                logger.error(_error.localizedDescription)
            }
        }
        self.session = nil
    }
}
```

しかしながら注意したいのは、

> // サンワサプライ NFCタグの特別対応？
// 先頭に\0が埋め込まれているので、その場合は除去する

と書いた部分です。これが書き込み用アプリケーションのバグなのか仕様なのか、または他社性では発生しないのかは分かりませんが、少なくとも今回はこの除去コードが必要でした。


最後に、これらのコードを使う場合は、下記のように書けばOKです！

```swift:App
class someClass {
    let nfc = NFCUtil(AppURLRouter())
 
    func pushed(_ sender: Any) {
        nfc.start()
    }
}

class AppURLRouter: URLRouter {
   func exec(query: URL) -> Bool {
     ...
     ...
   }
}
```

# 次のステップ

サーバサイドが未実装なので、これから取り組みますが、
Firebaseにすべきか、AWS RDS/Lambdaあたりにするかは思案のしどころです。
個人的にはGolangを使いたいので、ごりごりEC2＋Golang＋MySQLといった作戦もありそうです。

おっと！オフィスマップに表示するところも作らなくてはいけません。

社内のメンバーからAndroid版は作りますとの心強い宣言をいただいたので、それに間に合うように頑張りたいと思います。結果はまた記事にてご報告します！

# 参考Webサイト

[ RFIDとは？RFID活用事例27選と最新無人店舗化RFID動向【最新版】](https://ec-orange.jp/ec-media/?p=10839)
[RFIDとは (SK-Electronics Co.)](http://www.sk-el.co.jp/sales/rfid/about/index.html)
[RFIDとNFCの違いとは？](http://www.k-s-s.co.jp/column/rfid/about-rfid/rfid-nfc/)


[^Suica]: 「Suica」は東日本旅客鉄道株式会社の登録商標です。
[^楽天Edy]: 「楽天Edy」は楽天株式会社の登録商標です。