はじめに
iOS13でFeliCaが読み取れるようになりました。
身近にあるFeliCaといえばSuica。
iPhoneでSuicaを読み取って残高を表示するアプリをXamarin.Formsで作ってみました。
私は北海道民でKitacaしか持っていませんのでKitacaで動作確認しています。
ソースはこちら。
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は作れません。
ソリューションの作成
Xamarin.Formsソリューションを作成します。
今回作成したアプリはMVVMフレームワークにPrismを利用します。
参考:Prism Template Packを使わないでXamarin.FormsソリューションにPrismを適用する
iOSプロジェクトの設定
プロジェクトオプション
先ほど作成したNFCタグを読み取れるプロビジョニングプロファイルを設定します。
項目 | 設定内容 |
---|---|
プラットフォーム | 「iPhone」 |
プロビジョニングファイル | 先ほど作成したプロビジョニングプロファイル |
Info.plist
Info.plistの「ソース」タブで以下のプロパティを追加します。
Entitlements.plist
Entitlements.plistの「ソース」タブで以下のプロパティを追加します。
プログラム
今回作成したソースコードはGitHubに載せています。
https://github.com/ats-y/XamarinFelicaSample
以下、プログラムのポイントを記載していきます。
クラス構成
CoreNFCを利用したFeliCa読取処理はiOS依存なのでiOSプロジェクトのNfcServiceクラスで実装し、PCLからはDependencyServiceを使って呼び出します。
読取開始
/// <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()の引数は、読取成功時に呼び出すデリゲートです。
/// <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()
で読取画面が表示されます。
FeliCaタグ検知
タグを検知するとDidDetectTagsがコールバックされます。
/// <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()に表示したい文字列を指定して呼び出すとエラー画面が表示されます。
タグを検知したら、GetNFCFeliCaTag()でINFCFeliCaTagを実装したFeliCa対話用プロトコルを取得してFeliCaを制御します。
FeliCaの制御についてはFeliCa技術情報の「FeliCaカード ユーザーズマニュアル 抜粋版」などを参照するとよいと思います。
FeliCaと対話
残高情報を取得するために、Request ServiceコマンドでSuicaの履歴情報サービスを参照します。INFCFeliCaTag.RequestService()でRequest Serviceコマンドを送信すると、結果が引数で指定したデリゲートにコールバックされます。
/// <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引数に指定したデリゲートにコールバックされます。
/// <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()で読取成功画面を表示します。
読み取ったブロックデータから残高、日付を取り出します。
Suicaのデータ構成については、以下のサイトが詳しいです。
http://jennychan.web.fc2.com/format/suica.html
_onScanAction(remaining, useDate);
でStartScanningSuica()の引数で渡された読取成功時に呼び出すデリゲートを呼び出し、残高を表示しています。