iOSでNFCタグを読み取ってWi-Fi接続させる

iOS 11から使えるようになったCoreNFCフレームワークとNetworkExtensionフレームワークのNEHotspotConfiguration機能を使って実現します。
Androidと同じような感覚でNFCの機能を完全に使えるわけではなかったり、NFC(中でもNDEF)に関する理解が必要など、一筋縄ではいかないところもありますが、 **「iOSでAndroidと同じようなことできないの？」と言われることが多いと思われる** (私も含めた)iOSアプリ開発者の方々の参考になればと思います。

![NFC-WiFi.png](https://qiita-image-store.s3.amazonaws.com/0/16102/8381fec1-9835-afc7-6703-561a7c18429d.png "NFC-WiFi.png")

# 事前準備

CoreNFCではNFCタグにデータを書き込むことはできません(後述のとおり、NDEFメッセージを読み取ることしかできない)。したがって、NFCタグへのNDEFデータ書き込みについては別の手段を用意する必要があります。
今回は下記AndroidアプリとNFCタグを使って書き込みを行いました。

 - Androidアプリ: [NFC Tools](https://play.google.com/store/apps/details?id=com.wakdev.wdnfc&hl=ja)
 - NFCタグ: [MM-NFCT(サンワサプライ)](https://www.sanwa.co.jp/product/syohin.asp?code=MM-NFCT)

## 書き込みデータ

今回はWi-Fi設定情報を書き込みたいため、下図のように「Wi-Fi network」情報を書き込みました。この場合、NDEFにおけるType Name Format(以下、TNF)は`media`、Typeは`application/vnd.wfa.wsc`となります。

![NFCTools.png](https://qiita-image-store.s3.amazonaws.com/0/16102/b46b951d-59f4-2896-299e-d8e6a1317a5d.png "NFCTools.png")


# NetworkExtensionを使う

Capabilitiesにおいて`Hotspot Configuration`をONにして、`NetworkExtension`をimportします。
下記はViewController.swiftに実装したサンプルコードです。
Wi-Fi接続(W-Fiコンフィギュレーション適用)用メソッドとして`connect(_:passphrase:)`、切断(W-Fiコンフィギュレーション削除)用メソッドとして`disconnect(_:)`を用意します。
今回はWPAを想定しているため`isWEP`については`false`に設定しています。また、アプリ画面がバックグラウンドにある場合であってもWi-Fi接続できるように`NEHotspotConfiguration`の`joinOnce`プロパティについては`false`に設定しておきます(デフォルトは`false`のため、今回は設定する必要はありません)。

```swift
import UIKit
import NetworkExtension

class ViewController: UIViewController {

    var session: NFCNDEFReaderSession?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    private func connect(_ ssid: String, passphrase: String) {
        let hotspotConfig = NEHotspotConfiguration(ssid: ssid, passphrase: passphrase, isWEP: false)
        // hotspotConfig.joinOnce = false   // default
        
        NEHotspotConfigurationManager.shared.apply(hotspotConfig) { error in
            if let error = error {
                print("failed in applying Wi-Fi configuration, error: ", error.localizedDescription)
            } else {
                print("successful to apply Wi-Fi configuration")
            }
        }
    }
    
    private func disconnect(_ ssid: String) {
        NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid)
        print("removed Wi-Fi configuration")
    }
}
```


# CoreNFCを使う

Capabilitiesにおいて`Near Field Communication Tag Reading`をONにして、`CoreNFC`をimportします。また、下記の通りInfo.plistに`NFCReaderUsageDescription`を追加します。

```XML:Info.plist
<key>NFCReaderUsageDescription</key>
<string>This application uses NFC feature.</string>
```

## NDEFのパース

現実問題として、CoreNFCを使いこなすには、NDEFに関する仕様を理解(1)した上で、そのバイナリデータを適切に解析処理(2)する必要があります。が、後者(2)についてはOSSである[VYNFCKit](https://github.com/vinceyuan/VYNFCKit)を利用することでだいぶ負担軽減ができます。コード自体はObjective-Cで書かれており、Objective-C/Swiftの両方で利用可能で、CocoaPods/Carthageに対応しています。

## 完成サンプルコード

コードは多少見づらくなりますが、上記の`ViewControllewr.swift`にNFCタグを読み取り、Wi-Fi接続する処理を追加した完成コードが下記になります。
`IBAction`として追加した`scanTag(_:)`メソッドでNFCタグ読み取りの処理を開始、デリゲート設定しています。
コールバックされる`readerSession(_:didDetectNDEFs:)`メソッドにおいて、今回NFCタグに書き込んだWi-Fi設定データをパースする処理を実装しています。
`VYNFCNDEFPayloadParser.parse`で1次パース処理を行い、`VYNFCNDEFWifiSimpleConfigPayload`および`VYNFCNDEFWifiSimpleConfigCredential`で必要となるWi-Fi情報を取得しています。
今回は簡易的にSSIDとパスフレーズのみを取得した上で、`connect(_:passphrase:)`メソッドを呼び出し、Wi-Fi接続を実行してます。


```swift:ViewController.swift
import UIKit

import NetworkExtension

import CoreNFC
import VYNFCKit

class ViewController: UIViewController {

    var session: NFCNDEFReaderSession?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func scanTag(_ sender: UIButton) {
        session = NFCNDEFReaderSession(delegate: self, queue: .global(), invalidateAfterFirstRead: true)
        session?.alertMessage = "Keep iPhone close to the tag."
        session?.begin()
    }
    
    private func connect(_ ssid: String, passphrase: String) {
        let hotspotConfig = NEHotspotConfiguration(ssid: ssid, passphrase: passphrase, isWEP: false)
        // hotspotConfig.joinOnce = false   // default
        
        NEHotspotConfigurationManager.shared.apply(hotspotConfig) { error in
            if let error = error {
                print("failed in applying Wi-Fi configuration, error: ", error.localizedDescription)
            } else {
                print("successful to apply Wi-Fi configuration")
            }
        }
    }
    
    private func disconnect(_ ssid: String) {
        NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid)
        print("removed Wi-Fi configuration")
    }
}

extension ViewController: NFCNDEFReaderSessionDelegate {
    
    func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
        for message in messages {
            print("\(#function) - NFC messages count: \(messages.count)")
            
            for record in message.records {
                guard let parsedPayload = VYNFCNDEFPayloadParser.parse(record) else {
                    continue
                }
                if let wifi = parsedPayload as? VYNFCNDEFWifiSimpleConfigPayload {
                    for credential in wifi.credentials {
                        switch credential {
                        case let wificonfig as VYNFCNDEFWifiSimpleConfigCredential:
                            print("ssid: \(wificonfig.ssid)")
                            print("passphrase: \(wificonfig.networkKey)")
                            DispatchQueue.main.async {
                                self.connect(wificonfig.ssid, passphrase: wificonfig.networkKey)
                            }
                        default:
                            break
                        }
                    }
                }
            }
        }
    }
    
    func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
        print("\(#function) - error: \(error.localizedDescription)")
    }
}
```

# 最後に
いかがでしたでしょうか？CoreNFCを使いこなすにはNFC、NDEFに関する理解が不可欠です。
手始めにはQiita記事: [【NFC】NDEFについて理解する](https://qiita.com/shimosyan/items/ed21fb6984240baa7397)あたりが参考になるのではないかと思います。また、CoreNFCについては書籍: [iOS 11 Programming](https://peaks.cc/books/iOS11)が参考になりました。
皆さん、最新機能を使いながら開発者同士での情報共有、iOSフレームワークの改善に寄与していきましょう😄

## 参考
* Qiita記事:[【NFC】NDEFについて理解する](https://qiita.com/shimosyan/items/ed21fb6984240baa7397)
* 書籍:[iOS 11 Programming](https://peaks.cc/books/iOS11)