Swiftでネットワーク情報の取得(ローカルネットワークプライバシー、SSID、IPアドレス)

[qnote Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qnote) の3日目です。

---

とある案件にてアプリ内でネットワーク情報を取得して表示したいという要望があり実装する必要が出たためネットワークの情報取得処理を実装したので残しておきます。

##### 環境
Xcode 15.3
iPadOS 17

##### 要望
・インターネット接続状態
・Wi-Fi接続時に電波強度、SSID、IPアドレス
・ローカルネットワークプライバシーのパーミッション状態


##### 実装できたもの
・インターネット接続状態
・Wi-Fi接続時にSSID、IPアドレス
・ローカルネットワークプライバシーのパーミッション状態

---

# インターネット接続状態の取得
オンライン(セルラー、Wi-Fi)、オフラインの判定をしたかったため[Reachability](https://github.com/ashleymills/Reachability.swift)を使用しました。

使い方は色々なところで解説されているので割愛します。

# Wi-Fiの接続に情報
要望としては
・アクセスポイントの電波強度
・SSID
・ローカルIPアドレス
を取得したかったのですが、電波強度については調べて出てきたコードを動かしてみたのですが何故か正しい値が取得できなかったため断念しました。

・電波強度の取得方法
[NEHotspotNetworkにsignalStrength](https://developer.apple.com/documentation/networkextension/nehotspotnetwork/1618923-signalstrength)がありこちらを取得したのですが何故か0が返ってきてしまい正しい値が取得できませんでした。
※参考
[修羅の道 to iOSの電波強度取得](https://qiita.com/tosagatuo/items/6eb28363d04a6414051d)

・SSID
こちらも[NEHotspotNetworkにssid](https://developer.apple.com/documentation/networkextension/nehotspotnetwork/1618923-signalstrength)がありここからを取得しました。

[こちら](https://zenn.dev/usk2000/articles/9e890e160d2cb4)で大体解説されていますが
- CapabilitiiesにAccess WiFi Informationを追加
- 位置情報のパーミッションを許可している

が必要ですが位置情報についても色々なところで解説されているので割愛します。

```swift
    func fetchSSID() async {
        return await withCheckedContinuation { continuation in
            NEHotspotNetwork.fetchCurrent { network in
                guard let network else {
                    continuation.resume()
                    return
                }
                print("SSID: \(network.ssid), signalStrength: \(network.signalStrength)")
                continuation.resume()
            }
        }
    }
```

・IPアドレス
こちら[iOS端末ネットワークインターフェースのアドレス取得の実装と考察](https://qiita.com/manabapp/items/10b3b9b041759f3fac17)を参考に多少改良してあります。

```swift
    private func getIPAddress() {
        let statusBits: [UInt32] = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000]
        let statusLabels: [String] = ["UP", "BROADCAST", "DEBUG", "LOOPBACK", "POINTOPOINT", "SMART", "RUNNING", "NOARP", "PROMISC", "ALLMULTI", "OACTIVE", "SIMPLEX", "LINK0", "LINK1", "LINK2", "MULTICAST"]
        var ifaPtr: UnsafeMutablePointer<ifaddrs>? = nil
        var ifaListPtr: UnsafeMutablePointer<ifaddrs>? = nil
        guard getifaddrs(&ifaListPtr) == 0 else {
            return
        }
        defer {
            freeifaddrs(ifaListPtr)
        }
        ifaPtr = ifaListPtr
        while ifaPtr != nil {
            guard let ifa = ifaPtr?.pointee else {
                return
            }
            guard let ifaName = String(validatingUTF8: ifa.ifa_name) else {
                return
            }
            if ifa.ifa_addr.pointee.sa_family == Int32(AF_INET) {
                var output = "\(ifaName): " + String(format: "flags=%04x<", ifa.ifa_flags)
                var flg = false
                for i in 0 ..< statusBits.count {
                    if ifa.ifa_flags & statusBits[i] == statusBits[i] {
                        output += flg ? ",\(statusLabels[i])" : "\(statusLabels[i])"
                        flg = true
                    }
                }
                output += ">\n    "
                ifa.ifa_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
                    let add = String.init(cString: inet_ntoa(sin.pointee.sin_addr))
                    output += " inet " + add
                    if ifaName.contains("en0") {
                        // Wi-FiのIPアドレス (有線だともしかしたら取れないかも outputを確認)
                        print("IPアドレス: \(add)")
                    }
                }
                ifa.ifa_netmask.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
                    output += " netmask " + String.init(cString: inet_ntoa(sin.pointee.sin_addr))
                }
                if ifa.ifa_flags & UInt32(IFF_BROADCAST) == UInt32(IFF_BROADCAST) {
                    ifa.ifa_dstaddr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
                        output += " broadcast " + String.init(cString: inet_ntoa(sin.pointee.sin_addr))
                    }
                }
            }
            ifaPtr = ifa.ifa_next
        }
    }
```
ReachabilityにてWi-Fiに接続している際にこのメソッドを呼ぶようにしているためifaNameがen0の時にWi-Fiに接続している想定の実装になります。

# ローカルネットワークプライバシーのパーミッション状態
Stack Overflowにあった[こちらのコメント](https://stackoverflow.com/a/67758105)をConcurrencyに対応したGitHubの[こちら](https://gist.github.com/doozMen/0b5fc54c765bccb7c13792caa4eaa51c)を参考にさせて頂きました。  
何故かreadyのあたりやdelegateがうまく動かなかったためその辺りを書き換え(正しいのかどうかはわからないですが)たり、パーミッションの訴求ダイアログがまだ表示されていない場合に呼んだときにダイアログが出るように`ProcessInfo().hostName`でダイアログが出るようにしています。

参考ページにもありますがplistに`_bonjour._tcp`と`_lnp._tcp.`の記載が必要です。

```swift
import Network

class LocalNetworkAuthorization: NSObject {
    private var browser: NWBrowser?
    private var netService: NetService?
    private var completion: ((Bool) -> Void)?
    private var isCalledWaiting: Bool = false
    
    func requestAuthorization() async -> Bool {
        return await withCheckedContinuation { continuation in
            requestAuthorization() { result in
                continuation.resume(returning: result)
            }
        }
    }
    
    private func requestAuthorization(completion: @escaping (Bool) -> Void) {
        self.completion = completion
        
        _ = ProcessInfo().hostName
        
        // Create parameters, and allow browsing over peer-to-peer link.
        let parameters = NWParameters()
        parameters.includePeerToPeer = true
        
        // Browse for a custom service type.
        let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters)
        self.browser = browser
        browser.stateUpdateHandler = { newState in
            switch newState {
            case .failed(let error):
                print(error.localizedDescription)
            case .ready:
                // delegateが正しいっぽいけど呼ばれないのでここにきたら許可済みとする
                // 既に訴求済み(ダイアログが出ない)の場合でOFFに場合はreadyの後にwaitingが来るので少し待つ
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    if self.completion != nil {
                        self.reset()
                        self.completion?(true)
                    }
                }
            case .cancelled:
                break
            case .waiting(_):
                self.reset()
                self.completion?(false)
                self.completion = nil
            default:
                break
            }
        }
        
        self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
        self.netService?.delegate = self
        
        self.browser?.start(queue: .main)
        self.netService?.publish()
    }
    
    
    private func reset() {
        self.browser?.cancel()
        self.browser = nil
        self.netService?.stop()
        self.netService = nil
    }
}

extension LocalNetworkAuthorization: NetServiceDelegate {
    func netServiceDidPublish(_ sender: NetService) {
        print("LocalNetworkAuthorization delegate netServiceDidPublish()")
//        self.reset()
//        print("Local network permission has been granted")
//        completion?(true)
    }
}

```

##### 最後に

ローカルネットワークのパーミッションについてはあまり記事がなかったのと、他にも実装したものをまとめて記事にしたので後で必要になった方の参考にでもなればと思います。