2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

qnoteAdvent Calendar 2024

Day 3

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

Last updated at Posted at 2024-12-02

qnote Advent Calendar 2024 の3日目です。


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

環境

Xcode 15.3
iPadOS 17

要望

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

実装できたもの

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


インターネット接続状態の取得

オンライン(セルラー、Wi-Fi)、オフラインの判定をしたかったためReachabilityを使用しました。

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

Wi-Fiの接続に情報

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

・電波強度の取得方法
NEHotspotNetworkにsignalStrengthがありこちらを取得したのですが何故か0が返ってきてしまい正しい値が取得できませんでした。
※参考
修羅の道 to iOSの電波強度取得

・SSID
こちらもNEHotspotNetworkにssidがありここからを取得しました。

こちらで大体解説されていますが

  • CapabilitiiesにAccess WiFi Informationを追加
  • 位置情報のパーミッションを許可している

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

    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端末ネットワークインターフェースのアドレス取得の実装と考察を参考に多少改良してあります。

    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にあったこちらのコメントをConcurrencyに対応したGitHubのこちらを参考にさせて頂きました。
何故かreadyのあたりやdelegateがうまく動かなかったためその辺りを書き換え(正しいのかどうかはわからないですが)たり、パーミッションの訴求ダイアログがまだ表示されていない場合に呼んだときにダイアログが出るようにProcessInfo().hostNameでダイアログが出るようにしています。

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

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)
    }
}

最後に

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?