SwiftでWifiの電波強度を取得する方法です。
検索して調べたやり方で実装してみたところ、私の手元の環境ではうまく動きませんでした。
試行錯誤の末、自分なりに辿り着いた解決方法を記載します。
動作確認環境
機種 | iOSのバージョン |
---|---|
iPhone XS | 12.3.1 |
iPad 第5世代 | 12.1.4 |
WiFi情報はどこから取得するのか
参考サイトによると、ステータスバーの中にSignalに関する情報が入っているようです。
また、iPhoneX以降とそれ以前とではステータスバーの取得方法が異なるようです。
まずは試してみる
参考サイトのコードを真似して実装してみました。
iPhoneX以降用
private func getWifiNumberOfActiveBars() -> Int? {
let app = UIApplication.shared
var numberOfActiveBars: Int?
guard let containerBar = app.value(forKey: "statusBar") as? UIView else { return nil }
guard let statusBarMorden = NSClassFromString("UIStatusBar_Modern"), containerBar .isKind(of: statusBarMorden), let statusBar = containerBar.value(forKey: "statusBar") as? UIView else { return nil }
guard let foregroundView = statusBar.value(forKey: "foregroundView") as? UIView else { return nil }
for view in foregroundView.subviews {
for v in view.subviews {
if let statusBarWifiSignalView = NSClassFromString("_UIStatusBarWifiSignalView"), v .isKind(of: statusBarWifiSignalView) {
if let val = v.value(forKey: "numberOfActiveBars") as? Int {
numberOfActiveBars = val
break
}
}
}
if let _ = numberOfActiveBars {
break
}
}
return numberOfActiveBars
}
手元のiPhoneで確認してみたところ、こちらは想定通りの結果を得ることができました
iPhoneX以前用
private func getWiFiRSSI() -> Int? {
let app = UIApplication.shared
var rssi: Int?
guard let statusBar = app.value(forKey: "statusBar") as? UIView else { return nil }
if let statusBarMorden = NSClassFromString("UIStatusBar_Modern"), statusBar .isKind(of: statusBarMorden) { return nil }
guard let foregroundView = statusBar.value(forKey: "foregroundView") as? UIView else { return nil }
for view in foregroundView.subviews {
if let statusBarDataNetworkItemView = NSClassFromString("UIStatusBarDataNetworkItemView"), view .isKind(of: statusBarDataNetworkItemView) {
if let val = view.value(forKey: "wifiStrengthRaw3") as? Int {
rssi = val
break
}
}
}
return rssi
}
手元のiPadで確認してみたところ、常にnilが返されました・・・
原因
一行一行デバッグしてみたところ、6行目の if let statusBarMorden = NSClassFromString("UIStatusBar_Modern"), statusBar .isKind(of: statusBarMorden) { return nil }
の部分でreturnされていることがわかりました。
ホームボタンのない機種は UIStatusBar_Modern
が使われていて、それ以外の機種は UIStatusBar_Modern
が使われていないのだと勝手解釈していたのですが、違ったみたい。。
この辺イマイチわかっていないので、詳しい方教えて欲しいです。
色々ためしてみる
iPadでも UIStatusBar_Modern
が取れるのなら、実がiPhoneX以降用のやり方で電波強度を取得できるのかなと思い、先ほど記載したiPhoneX以降用の関数を呼んでみました。
結果は・・・ダメでした
どうやらStatusbarの階層構造が違うようです。
iPhoneXSだと、以下のように foregroundView > subview > subview
の階層に _UIStatusBarWifiSignalView
がいるのに対し、
<UIView: 0x1066024c0; frame = (13.3333 14.6667; 66.6667 13.6667); layer = <CALayer: 0x280b6cbe0>>
<_UIStatusBarWifiSignalView: 0x105602b00; frame = (22 2.66667; 15.3333 11); userInteractionEnabled = NO; layer = <CALayer: 0x280b660a0>>
iPadだと、以下のように foregroundView > subview
の階層に _UIStatusBarWifiSignalView
がいました。
<_UIStatusBarWifiSignalView: 0x15de17910; frame = (672.5 5; 14 10); userInteractionEnabled = NO; layer = <CALayer: 0x28278a0e0>>
苦肉の策
iPhoneX以前用の関数内で、 UIStatusBar_Modern
がある場合とない場合とで処理を分けてみました。
static func getWifiRSSI() -> Int? {
let app = UIApplication.shared
var numberOfActiveBars: Int?
guard let containerBar = app.value(forKey: "statusBar") as? UIView else { return 0 }
if let statusBarMorden = NSClassFromString("UIStatusBar_Modern"), containerBar.isKind(of: statusBarMorden) {
guard let statusBar = containerBar.value(forKey: "statusBar") as? UIView else { return 0 }
guard let foregroundView = statusBar.value(forKey: "foregroundView") as? UIView else { return 0 }
for view in foregroundView.subviews {
if let statusBarWifiSignalView = NSClassFromString("_UIStatusBarWifiSignalView"), view
.isKind(of: statusBarWifiSignalView) {
if let val = view.value(forKey: "numberOfActiveBars") as? Int {
numberOfActiveBars = val
break
}
}
if let _ = numberOfActiveBars {
break
}
}
} else {
guard let foregroundView = containerBar.value(forKey: "foregroundView") as? UIView else { return 0 }
for view in foregroundView.subviews {
if let statusBarDataNetworkItemView = NSClassFromString("UIStatusBarDataNetworkItemView"), view .isKind(of: statusBarDataNetworkItemView) {
if let val = view.value(forKey: "wifiStrengthRaw") as? Int {
numberOfActiveBars = val
}
}
}
}
return numberOfActiveBars
}
これで無事にiPadでもWifiの電波強度を取得することができました。
では、else にいくのはどういう時か
UIStatusBar_Modern
存在有無の条件分岐を入れてみたものの、ここで else
にくるのは、どういったケースなのかが謎でした。
周りにいるiPhoneユーザの方々に協力してもらったところ、iPhoneSEやiPhone5の場合に else
の処理に入ってくることがわかりました。
そしてこのソースでiPhoneSEでも無事に電波強度を取得することができたのですが、デバッグしていて一つ疑問が残りました。
残された疑問
手元のiPhoneX以降・iPadでは、返される電波強度は「0〜4」の数値(ステータスバーの扇の数)であるのに対して、iPhoneSEでは「-67」とか「-73」のように、dBmと思われる値が返されました。
この辺は端末の仕様によるものなのでしょうか。。。
私は4段階での数値が必要だったため、ここでもケース分けしなければならず、かなり汚いソースになってしまいました
一応想定通り動いているとはいえ、なんとなくスッキリしないので、もっと良い方法ご存知の方いらっしゃればぜひご教示いただきたいです
2019/11/25 追記
上述の方法はXcode11ではビルドエラーが発生するようになりました。
そのため「statusBarManager」を使用したやり方に修正する必要があります。
私は以下のように記載し、とりあえずは意図した通りに動いておりますが、
かなり手探りであまり自信がないので、もし間違い等あればご指摘ください。
// iOS13対応
if #available(iOS 13.0, *) {
if let statusBarManager = UIApplication.shared.keyWindow?.windowScene?.statusBarManager,
let localStatusBar = statusBarManager.perform(Selector(("createLocalStatusBar")))?.takeUnretainedValue() as? UIView,
let statusBarMorden = localStatusBar.perform(Selector(("statusBar")))?.takeUnretainedValue() as? UIView,
let stb = statusBarMorden.value(forKey: "_statusBar") as? UIView {
guard let currentData = stb.value(forKeyPath: "currentData") else { return 0 }
let wifiEntry = (currentData as AnyObject).value(forKeyPath: "wifiEntry")
numberOfActiveBars = (wifiEntry as AnyObject).value(forKeyPath: "displayValue") as? Int
}
} else {
// 〜 iOS13未満は上述のやり方で取得できるため省略 〜
}
return numberOfActiveBars
print(wifiEntry)
をしてみたところ以下のようにデータが格納されていました。
Optional(<_UIStatusBarDataWifiEntry: 0x2806ccc00: isEnabled=1, rawValue=0, displayValue=2, displayRawValue=0, status=5, lowDataModeActive=0, type=0>)
displayValue
がステータスバーに表示されている電波強度なので、これを返してあげれば良いです。
ちなみに、LTEもほとんどやり方は同じで、以下のようにKeyのwifiEntry
をcellularEntry
にするだけでした。
let cellularEntry = (currentData as AnyObject).value(forKeyPath: "cellularEntry")
ご参考まで。。。
※ XCode(11.2.1)で作成、iPhoneXS(13.2.2)で動作確認済みです。