LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-06

はじめに

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

  1. ボタンタップでNFC読取画面を表示。
    001.png → 002.png

  2. Kitaka/SuicaをiPhoneの背面上部にかざし、読取が成功したら残高を表示する。
    003.png004.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」にチェックをつける。
    (無償のDevelopers Programでは選択できません)

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

作成したApp IDがこちら。
image.png

作成したIDでプロビジョニングプロファイルを作成します。
image.png

ソリューションの作成

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

参考:Prism Template Packを使わないでXamarin.FormsソリューションにPrismを適用する

iOSプロジェクトの設定

プロジェクトオプション

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

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

image.png

Info.plist

Info.plistの「ソース」タブで以下のプロパティを追加します。
image.png

Entitlements.plist

Entitlements.plistの「ソース」タブで以下のプロパティを追加します。
image.png

プログラム

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

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

クラス構成

ClassDiagram.png

CoreNFCを利用したFeliCa読取処理はiOS依存なのでiOSプロジェクトのNfcServiceクラスで実装し、PCLからはDependencyServiceを使って呼び出します。

読取開始

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()の引数は、読取成功時に呼び出すデリゲートです。

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

FeliCaタグ検知

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

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
タグを検知したら、GetNFCFeliCaTag()でINFCFeliCaTagを実装したFeliCa対話用プロトコルを取得してFeliCaを制御します。

FeliCaの制御についてはFeliCa技術情報の「FeliCaカード ユーザーズマニュアル 抜粋版」などを参照するとよいと思います。

FeliCaと対話

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

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引数に指定したデリゲートにコールバックされます。

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
読み取ったブロックデータから残高、日付を取り出します。

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

_onScanAction(remaining, useDate);でStartScanningSuica()の引数で渡された読取成功時に呼び出すデリゲートを呼び出し、残高を表示しています。
004.png

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2