【WWDC19】Core NFC で FeliCa(Suica) を読み取るサンプル【iOS 13 以降】

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">お、iOS 13 の Core NFC、<br>ついに FeliCa、MIFARE タグの読み取りができるようになるみたい！！！<a href="https://twitter.com/hashtag/WWDC19?src=hash&amp;ref_src=twsrc%5Etfw">#WWDC19</a> <a href="https://twitter.com/hashtag/iOS13?src=hash&amp;ref_src=twsrc%5Etfw">#iOS13</a> <a href="https://twitter.com/hashtag/NFC?src=hash&amp;ref_src=twsrc%5Etfw">#NFC</a> <a href="https://twitter.com/hashtag/FeliCa?src=hash&amp;ref_src=twsrc%5Etfw">#FeliCa</a> <a href="https://t.co/C1UtAV2byr">pic.twitter.com/C1UtAV2byr</a></p>&mdash; treastrain / とれいん (@treastrain) <a href="https://twitter.com/treastrain/status/1135705269248200704?ref_src=twsrc%5Etfw">June 4, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

iOS 13 以降の Core NFC では FeliCa の読み取りに対応したことがわかったので、読み取るためのサンプルコードを載せます。

# サンプルコード
```swift
//  Created by treastrain on 2019/06/06.
//  Copyright © 2019 treastrain / Tanaka Ryoga. All rights reserved.
//

import UIKit
import CoreNFC

class ViewController: UIViewController, NFCTagReaderSessionDelegate {

    var session: NFCTagReaderSession?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
    @IBAction func beginScanning(_ sender: UIButton) {
        guard NFCTagReaderSession.readingAvailable else {
            let alertController = UIAlertController(
                title: "Scanning Not Supported",
                message: "This device doesn't support tag scanning.",
                preferredStyle: .alert
            )
            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alertController, animated: true, completion: nil)
            return
        }
        
        self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
        self.session?.alertMessage = "Hold your iPhone near the item to learn more about it."
        self.session?.begin()
    }
    
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        print("tagReaderSessionDidBecomeActive(_:)")
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
        if let readerError = error as? NFCReaderError {
            if (readerError.code != .readerSessionInvalidationErrorFirstNDEFTagRead)
                && (readerError.code != .readerSessionInvalidationErrorUserCanceled) {
                let alertController = UIAlertController(
                    title: "Session Invalidated",
                    message: error.localizedDescription,
                    preferredStyle: .alert
                )
                alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                DispatchQueue.main.async {
                    self.present(alertController, animated: true, completion: nil)
                }
            }
        }
        
        self.session = nil
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        print("tagReaderSession(_:didDetect:)")
        
        if tags.count > 1 {
            let retryInterval = DispatchTimeInterval.milliseconds(500)
            session.alertMessage = "More than 1 tag is detected, please remove all tags and try again."
            DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
                session.restartPolling()
            })
            return
        }
        
        let tag = tags.first!
        
        session.connect(to: tag) { (error) in
            if nil != error {
                session.invalidate(errorMessage: "Connection error. Please try again.")
                return
            }
            
            guard case .feliCa(let feliCaTag) = tag else {
                let retryInterval = DispatchTimeInterval.milliseconds(500)
                session.alertMessage = "A tag that is not FeliCa is detected, please try again with tag FeliCa."
                DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
                    session.restartPolling()
                })
                return
            }
            
            print(feliCaTag)
            
            let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
            let systemCode = feliCaTag.currentSystemCode.map { String(format: "%.2hhx", $0) }.joined()
            
            print("IDm: \(idm)")
            print("System Code: \(systemCode)")
            
            session.alertMessage = "Read success!\nIDm: \(idm)\nSystem Code: \(systemCode)"
            session.invalidate()
        }
    }


}
```

# 設定事項
Core NFC を使うにあたって、コードだけでなく Xcode プロジェクトの設定もいくつか必要です。
特に、読み取りたい FeliCa のシステムコードをあらかじめ全て指定しておく必要があります。
[こちらの記事](https://notes.tret.jp/ios13-felica-reading)を参照してください。

# 動作確認
FeliCa カードを読み取ることはもちろん、Apple Watch の Suica であっても（エミュレートカード）読み取ることができます。

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/fPmtqwH0Jys" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# サンプルプロジェクト
このコードが使用されているサンプルプロジェクトを GitHub にて公開しております。
[treastrain/ios13-felica-reader: Sample project to read FeliCa on iOS 13 and later - GitHub](https://github.com/treastrain/ios13-felica-reader)