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