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



こんにちは、怠けるのが好きで SwiftUI が大好きな [aBiteエンジニア](https://www.abite.tech)、けわしです！

#TL;DR
- 交通系ICカード、Suica、PASMO、ICOCA の残高を読み取りましょう🚃
- Xcode 11 で導入された最新技術、**SwiftUI** と **Swift Package Manager (SPM)** を使って、**爆速開発**します🏇😝 
- 私は**30分でアプリ完成**しました⏱
- [WAON, nanaco, 楽天Edy の電子マネー](https://qiita.com/treastrain/items/f5c518486d19ba3573d3) や[運転免許証](https://qiita.com/Kewa4/items/02217a5f2396b70acf83)の読み取りの練習にご活用ください💶💰
- iPhone で IC 使うには [**TRETJapanNFCReader**](https://github.com/treastrain/TRETJapanNFCReader) が最高です！ (@treastrain 氏開発のOSSライブラリ)

#完成するアプリ
Suica、PASMO、ICOCA タッチで、残高が見れます！
<img width="225" alt="IMG_02589DDD55D5-1.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/718509c3-9fd2-282a-499a-05a677a2ccd3.jpeg"><img width="239" alt="Screenshot 2019-12-15 at 3.03.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/155b9d19-5cb7-7f7d-0f13-76dbe77c8b78.png">

#Xcode プロジェクトの作成
Create a new Xcode project から iOS の Single View App を作成します。
User Interface は忘れず SwiftUI にしましょう！
<img width="1000" alt="Screenshot 2019-12-15 at 3.08.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/7c44cf10-1152-a5a9-cba1-9f5101c463ea.png">

# プロジェクト設定 と Info.plist
プロジェクト設定で、capabilities を選択します。
<img width="1000" alt="Screenshot 2019-12-15 at 2.20.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/76ca392f-0ad6-b52f-c70e-04faf6faefd0.png">
NFC を追加します！
<img width="1000" alt="Screenshot 2019-12-15 at 2.22.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/259fa6bf-56d0-e006-0c6e-54d93e4c29ce.png">
Info.plist に次の設定を記入してください。
<img width="1000" alt="Screenshot 2019-12-15 at 3.12.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/e722717e-3534-1b0a-a0d3-41aab1c1dc5d.png">
Privacy - NFC Scan Usage Description に、何か文字列を与えるのはとても大事で、ないとアプリがクラッシュします！逆にクラッシュしたら、これで直ること、多いですね！
0003 は交通系ICカード用のFeliCa システムコードですね。これがないとタッチしても反応しません。

# ファイルツリー
<img width="208" alt="Screenshot 2019-12-15 at 3.19.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/236d20ea-250f-ade8-0fcb-e4d2b5ebace1.png">
赤字のファイルを触ります！

# Swift Package Manager でライブラリインストール
Swift Package Manager で開き
<img width="1000" alt="Screenshot 2019-12-15 at 2.28.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/dfc35c8e-fc63-42bb-3bd6-0a4d3d134f89.png">

```TRETJapanNFCReader``` を検索します。
<img width="1000" alt="Screenshot 2019-12-15 at 2.29.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/3c7eaec8-8276-992f-779a-06b5f12e3f46.png">

あとは、指示に従い、インストール。
無事インストール完了すると、次のようになります。
<img width="1000" alt="Screenshot 2019-12-15 at 3.28.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/9c4bbbeb-db6c-4b08-b85c-6f3bed0ba1a5.png">

#コーディング
##コード1: Suica読み取り部分
```swift: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: アプリ画面部分
```swift: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行目のコード

```swift:SceneDelegate.swift
let contentView = ContentView()
```
を、次のように変更します。

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

## コード解説
### アプリ画面部分：ContentView.swift
#### EnvironmentObject
```swift
@EnvironmentObject var userData: UserData
```
UserData 側の残高変更を監視する為、"@EnvironmentObject" をつけます。

#### 残高表示 <img width="103" alt="Screenshot 2019-12-15 at 3.47.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/6d1c17b8-6966-3e50-ba6a-3f0b13faf1bf.png">

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

#### プレビュー

```swift:ContentView.swift
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let userData = UserData()
        userData.balance = 1500 // プレビュー用残高
        return ContentView().environmentObject(userData)
    }
}
```
こうすることで、プレビューがでます。
<img width="1072" alt="Screenshot 2019-12-15 at 3.50.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/ead456dc-7282-9708-8b65-14f58a15f59c.png">
プレビューの良いところは、要素を選択して右のペインでフォント色などを変更すると、コードが自動で変わるところですね😍
<img width="1000" alt="Screenshot 2019-12-15 at 3.53.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275966/6b9d10d2-bfa0-fa39-2b38-8d84caf59a85.png">



### Suica読み取り部分：UserData.swift
#### スキャン開始
アプリ起動時に、`SceneDelegate.swift` で `UserData()` が呼ばれた時に読み取りを開始します。

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

#### Suica 残高読み取り
```swift: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.）を読み取ってみよう！](https://qiita.com/treastrain/items/d54ef2c2abbcd40fb9f1)
[treastrain/TRETJapanNFCReader](https://github.com/treastrain/TRETJapanNFCReader) 今回用いているOSSライブラリ。とてもよいライブラリです。