30分で爆速開発 交通系ICリーダー‬ 【iOS, SwiftUI】


こんにちは、怠けるのが好きで SwiftUI が大好きな aBiteエンジニア、けわしです!

TL;DR

  • 交通系ICカード、Suica、PASMO、ICOCA の残高を読み取りましょう🚃
  • Xcode 11 で導入された最新技術、SwiftUISwift Package Manager (SPM) を使って、爆速開発します🏇😝
  • 私は30分でアプリ完成しました⏱
  • WAON, nanaco, 楽天Edy の電子マネー運転免許証の読み取りの練習にご活用ください💶💰
  • iPhone で IC 使うには TRETJapanNFCReader が最高です! (@treastrain 氏開発のOSSライブラリ)

完成するアプリ

Suica、PASMO、ICOCA タッチで、残高が見れます!

Xcode プロジェクトの作成

Create a new Xcode project から iOS の Single View App を作成します。
User Interface は忘れず SwiftUI にしましょう!

プロジェクト設定 と Info.plist

プロジェクト設定で、capabilities を選択します。

NFC を追加します!

Info.plist に次の設定を記入してください。

Privacy - NFC Scan Usage Description に、何か文字列を与えるのはとても大事で、ないとアプリがクラッシュします!逆にクラッシュしたら、これで直ること、多いですね!
0003 は交通系ICカード用のFeliCa システムコードですね。これがないとタッチしても反応しません。

ファイルツリー


赤字のファイルを触ります!

Swift Package Manager でライブラリインストール

Swift Package Manager で開き

TRETJapanNFCReader を検索します。

あとは、指示に従い、インストール。
無事インストール完了すると、次のようになります。

コーディング

コード1: Suica読み取り部分

UserData.swift
import Foundation
import TRETJapanNFCReader

final class UserData: NSObject, ObservableObject, FeliCaReaderSessionDelegate {
    @Published var balance: Int? = nil
    var reader: TransitICReader!

    override init() {
        super.init()
        self.reader = TransitICReader(delegate: self)
        self.reader.get(itemTypes: [.balance])
    }

    func feliCaReaderSession(didRead feliCaCard: FeliCaCard) {
        let transitICCard = feliCaCard as! TransitICCard
        DispatchQueue.main.async {
            self.balance = transitICCard.data.balance! // カード残高
        }
    }

    // ライブラリを使う上で、FelicaReaderSessionDelegate の要求する型に合わせる為、記述しております。
    func japanNFCReaderSession(didInvalidateWithError error: Error) {
    }
}

コード2: アプリ画面部分

ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        VStack {
            Text("FeliCa の残高")
            if (self.userData.balance != nil) {
                Text(\(String(self.userData.balance!))")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let userData = UserData()
        userData.balance = 1500 // プレビュー用残高
        return ContentView().environmentObject(userData)
    }
}

コード3: 仕上げ

ContentView に UserData をくっつけます。
SceneDelegate.swift の23行目のコード

SceneDelegate.swift
let contentView = ContentView()

を、次のように変更します。

SceneDelegate.swift
let contentView = ContentView().environmentObject(UserData())

これだけで、アプリ起動時にスキャナが起動します。そして、読み取り成功後に、残高をアプリ画面(ContentView) に渡してくれます☺️

コード解説

アプリ画面部分:ContentView.swift

EnvironmentObject

@EnvironmentObject var userData: UserData

UserData 側の残高変更を監視する為、"@EnvironmentObject" をつけます。

残高表示

ContentView.swift
var body: some View {
    // 縦に2つ並べる
    VStack {
       Text("FeliCa の残高")
       // 残高情報がある場合に、残高を表示する
       if (self.userData.balance != nil) {
            Text(\(String(self.userData.balance!))")
       }
    }
}

プレビュー

ContentView.swift
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let userData = UserData()
        userData.balance = 1500 // プレビュー用残高
        return ContentView().environmentObject(userData)
    }
}

こうすることで、プレビューがでます。

プレビューの良いところは、要素を選択して右のペインでフォント色などを変更すると、コードが自動で変わるところですね😍

Suica読み取り部分:UserData.swift

スキャン開始

アプリ起動時に、SceneDelegate.swiftUserData() が呼ばれた時に読み取りを開始します。

UserData.swift
    override init() {
        super.init()
        self.reader = TransitICReader(delegate: self)
        self.reader.get(itemTypes: [.balance])
    }

Suica 残高読み取り

UserData.swift
    func feliCaReaderSession(didRead feliCaCard: FeliCaCard) {
        let transitICCard = feliCaCard as! TransitICCard
        DispatchQueue.main.async {
            self.balance = transitICCard.data.balance! // カード残高
        }
    }

ポイントは、self.balance = transitICCard.data.balance!DispatchQueue.main.async {} で囲うところですね。
これは、UserData と ContentView が別スレッドで走っています。これで囲うと、別スレッドのContentViewに残高が送信できます。

参考、おすすめ文献

iPhone で交通系IC(Suica、PASMO、ICOCA、…etc.)を読み取ってみよう!
treastrain/TRETJapanNFCReader 今回用いているOSSライブラリ。とてもよいライブラリです。