Xamarin.FormsでFeliCaを読み取るiOSアプリを作ってみた

# はじめに

iOS13でFeliCaが読み取れるようになりました。
身近にあるFeliCaといえばSuica。
iPhoneでSuicaを読み取って残高を表示するアプリをXamarin.Formsで作ってみました。
私は北海道民でKitacaしか持っていませんのでKitacaで動作確認しています。

1. ボタンタップでNFC読取画面を表示。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/2eef1de0-d5e0-848a-d175-41082623ce78.png)　→　![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/3b052bbf-fd60-91d1-6cbd-1e0327e0dfca.png)

1. Kitaka/SuicaをiPhoneの背面上部にかざし、読取が成功したら残高を表示する。
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/8c55fd62-f5d8-e694-c63f-89b0b9d1ea08.png) → ![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/da31746d-8d44-569e-b311-b778c4324b91.png)

ソースはこちら。
https://github.com/ats-y/XamarinFelicaSample

# 環境

### 開発環境
Visual Studio 2019 for MAC
iPhone 8 ( iOS 13.4.1 ) **★iPhone実機が必要★**

### 依存関係
Xamarin.Forms 4.3
Prism.Unity.Forms 7.2

### その他必要なもの
Apple Developers Program ( ¥11,800 税抜き )
Kitaca ( SuicaでもOK )

# 作り方

## NFCタグを読み取れるプロビジョニングプロファイルの作成

Apple DeveloperのAccountページから「Certificates, Identifiers & Profiles」を開き、NFC読取機能付きのApp IDを作ります。ポイントは次の通りです。

* Bundle IDはexplicitにする。
* Capabilitiesで「NFC Tag Reading」にチェックをつける。<br>（無償のDevelopers Programでは選択できません）

ちなみにCapabilitiesでNFC Tag ReadingをつけるとwildcardのBundle IDは作れません。

作成したApp IDがこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/b6ffbc6d-33fe-2f58-82df-18209cb7fd7b.png)

作成したIDでプロビジョニングプロファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/a6af1744-5755-1895-6583-41c1ed518af1.png)


## ソリューションの作成

Xamarin.Formsソリューションを作成します。
今回作成したアプリはMVVMフレームワークにPrismを利用します。

参考：[Prism Template Packを使わないでXamarin.FormsソリューションにPrismを適用する](https://qiita.com/ats_y/items/f76098612786b3e9cf4a)

## iOSプロジェクトの設定

### プロジェクトオプション
先ほど作成したNFCタグを読み取れるプロビジョニングプロファイルを設定します。

|項目|設定内容|
|---|---|
|プラットフォーム|「iPhone」|
|プロビジョニングファイル|先ほど作成したプロビジョニングプロファイル|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/ba939f9d-92b0-ab38-35d0-5bbb9f8d3e0e.png)

### Info.plist
Info.plistの「ソース」タブで以下のプロパティを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/3e505a93-88a8-8a97-32c6-c79bf002749a.png)

### Entitlements.plist
Entitlements.plistの「ソース」タブで以下のプロパティを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/223e122a-04a3-ef6f-6c7e-8bc7f7a7d48a.png)

## プログラム

今回作成したソースコードはGitHubに載せています。
https://github.com/ats-y/XamarinFelicaSample

以下、プログラムのポイントを記載していきます。

### クラス構成

<img width="436" alt="ClassDiagram.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/47fa5715-8995-326a-6e0f-c09c27518644.png">

CoreNFCを利用したFeliCa読取処理はiOS依存なのでiOSプロジェクトのNfcServiceクラスで実装し、PCLからは[DependencyService](https://docs.microsoft.com/ja-jp/xamarin/xamarin-forms/app-fundamentals/dependency-service/)を使って呼び出します。

### 読取開始

```C#:NfcSamples.ViewModels.MainPageViewModel.cs
/// <summary>
/// NFC読取開始コマンドイベントハンドラ
/// </summary>
private void OnStartScanningCommand()
{
    // 画面上の残高情報を隠す。
    IsVisibleRemaining = false;
    Remaining = 0;
    UseDate = DateTime.MinValue;

    // NFCの読取を開始する。
    INfcService nfcService = DependencyService.Get<INfcService>();
    nfcService.StartScanningSuica((remaining, useDate) =>
    {
        // 読取に成功したら読み取った残高を表示する。
        Remaining = remaining;
        UseDate = useDate;
        IsVisibleRemaining = true;
    });
}
```

OnStartScanningCommand()が読取開始ボタンのイベントハンドラです。
DependencyServiceでiOSのNfcServiceクラスのFeliCa読取処理StartScanningSuica()を呼び出しています。
StartScanningSuica()の引数は、読取成功時に呼び出すデリゲートです。

```C#:NfcSamples.iOS.NfcService.NfcService.cs
/// <summary>
/// SuiCa残高をスキャンした際に呼び出す。
/// int引数は読み取った残高。
/// </summary>
private Action<int, DateTime> _onScanAction;

/// <summary>
/// NFCタグ読取セッション
/// </summary>
private NFCTagReaderSession _session;

/// <summary>
/// Suica読取開始。
/// </summary>
/// <param name="onScanAction">Suica検知デリゲート。引数はSuicaから読み取った残高と日付。</param>
public void StartScanningSuica(Action<int, DateTime> onScanAction)
{
    // Suica検知デリゲートを保存。
    _onScanAction = onScanAction;

    // NFC読取セッションを開始する。
    _session = new NFCTagReaderSession(NFCPollingOption.Iso18092, this, DispatchQueue.CurrentQueue);
    _session.AlertMessage = "Suica/Kitacaをかざして";
    _session.BeginSession();
}
```

`_session.AlertMessage`で指定している文字列が読取画面に表示される文字列で、`_session.BeginSession()`で読取画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/f22c617d-0b9b-6bd6-2b79-75a1ec196bc4.png)

### FeliCaタグ検知

タグを検知するとDidDetectTagsがコールバックされます。

```C#:NfcSamples.iOS.NfcService.NfcService.cs
/// <summary>
/// FeliCaタグ
/// </summary>
private INFCFeliCaTag _felicaTag;

/// <summary>
/// Suica履歴のサービスコード
/// （リトルエンディアン）
/// </summary>
private static readonly NSData[] ServiceCodes = { NSData.FromArray(new byte[] { 0x0F, 0x09 }) };

/// <summary>
/// NFC読取セッションがタグを検知されたら呼び出される。
/// </summary>
/// <param name="session">NFC読取セッション</param>
/// <param name="tags">検知タグ</param>
public override void DidDetectTags(NFCTagReaderSession session, INFCTag[] tags)
{
    Debug.WriteLine($"DidDetectTags");

    // タグに接続する。
    if (tags.Length <= 0) return;
    session.ConnectTo(tags[0], connectErr =>
    {
        // 接続エラー時はメッセージを表示してNFC読取セッションを終了する、
        if (connectErr != null)
        {
            Debug.WriteLine($"Connect Error = [{connectErr}]");
            session.InvalidateSession("タグ接続失敗。");
            return;
        }

        // FeliCa準拠のタグプロトコルを取得する。
        _felicaTag = tags[0].GetNFCFeliCaTag();
        if (_felicaTag == null) return;

        // FeliCaのRequest Serviceコマンドを実行し、
        // サービスコード0x090Fを指定し、カード種別およびカード残額情報サービスに接続する。
        // （サービスコードはリトルエンディアンで）
        _felicaTag.RequestService(ServiceCodes, OnCompletedRequestService);
    });
}
```

読取に失敗した場合はNFCTagReaderSession.InvalidateSession()に表示したい文字列を指定して呼び出すとエラー画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/0c5a1db2-4553-2f42-fa70-8b0042fb9fdd.png)
タグを検知したら、GetNFCFeliCaTag()で[INFCFeliCaTagを実装したFeliCa対話用プロトコル](https://developer.apple.com/documentation/corenfc/nfcfelicatag)を取得してFeliCaを制御します。

*FeliCaの制御については[FeliCa技術情報](https://www.sony.co.jp/Products/felica/business/tech-support/)の「FeliCaカード ユーザーズマニュアル 抜粋版」などを参照するとよいと思います。*

### FeliCaと対話

残高情報を取得するために、Request ServiceコマンドでSuicaの履歴情報サービスを参照します。INFCFeliCaTag.RequestService()でRequest Serviceコマンドを送信すると、結果が引数で指定したデリゲートにコールバックされます。

```C#:NfcSamples.iOS.NfcService.NfcService.cs
/// <summary>
/// FeliCaのRequest Serviceコマンドのレスポンス受信ハンドラ
/// </summary>
/// <param name="nodeVersions">ノード鍵バージョンリスト</param>
/// <param name="err">エラー</param>
private void OnCompletedRequestService(NSData[] nodeVersions, NSError err)
{
    // 略

    // ReadWithoutEncryptionコマンドで
    // ブロック番号0〜11の12個分のブロックを読み取るブロックリストを作成する。
    List<NSData> readBlocks = new List<NSData>();
    for (int i = 0; i < 12; i++)
    {
        byte[] block = new byte[] { 0x80, (byte)i };
        readBlocks.Add(NSData.FromArray(block));
    }

    // ブロックデータを読み出す。
    _felicaTag.ReadWithoutEncryption(ServiceCodes
        , readBlocks.ToArray()
        , OnCompletedReadWithoutEncryption);
}
```

Suica/Kitacaの残高を読み取るため、FeliCaのReadWithoutEncryptionコマンドを送信します。
コマンドの結果は、INFCFeliCaTag.ReadWithoutEncryption()の第2引数に指定したデリゲートにコールバックされます。

```C#:NfcSamples.iOS.NfcService.NfcService.cs
/// <summary>
/// FeliCaのRead Without Encryptionコマンドのレスポンス受信ハンドラ
/// </summary>
/// <param name="statusFlag1">ステータスフラグ1</param>
/// <param name="statusFlag2">ステータスフラグ2</param>
/// <param name="blockData">ブロックデータ</param>
/// <param name="error">エラー</param>
private void OnCompletedReadWithoutEncryption(nint statusFlag1, nint statusFlag2, NSData[] blockData, NSError error)
{
    // 略

    // 読取セッションを終了する。画面には成功イメージが表示される。
    _session.InvalidateSession();

    // 読み取ったブロックデータから残高を取り出す。
    byte[] readBytes = blockData[0].ToArray();
    int year = (readBytes[4] >> 1) + 2000;
    int month = ((readBytes[4] & 1) == 1 ? 8 : 0) + (readBytes[5] >> 5);
    int day = readBytes[5] & 0x1f;
    int remaining = readBytes[10] + (readBytes[11] << 8);
    DateTime useDate = new DateTime(year, month, day);
    Debug.WriteLine($"{year}年{month}月{day}日 {remaining}円");

    // 残高を通知する。
    _onScanAction(remaining, useDate);
}
```
NFCTagReaderSession.InvalidateSession()で読取成功画面を表示します。
![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/8c55fd62-f5d8-e694-c63f-89b0b9d1ea08.png)
読み取ったブロックデータから残高、日付を取り出します。

*Suicaのデータ構成については、以下のサイトが詳しいです。
http://jennychan.web.fc2.com/format/suica.html*

`_onScanAction(remaining, useDate);`でStartScanningSuica()の引数で渡された読取成功時に呼び出すデリゲートを呼び出し、残高を表示しています。
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342629/da31746d-8d44-569e-b311-b778c4324b91.png)