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

NFC-WiFi.png

事前準備

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

書き込みデータ

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

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接続できるようにNEHotspotConfigurationjoinOnceプロパティについてはfalseに設定しておきます(デフォルトはfalseのため、今回は設定する必要はありません)。

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を追加します。

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

NDEFのパース

現実問題として、CoreNFCを使いこなすには、NDEFに関する仕様を理解(1)した上で、そのバイナリデータを適切に解析処理(2)する必要があります。が、後者(2)についてはOSSである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接続を実行してます。

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について理解するあたりが参考になるのではないかと思います。また、CoreNFCについては書籍: iOS 11 Programmingが参考になりました。
皆さん、最新機能を使いながら開発者同士での情報共有、iOSフレームワークの改善に寄与していきましょう😄

参考