8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftでWiFi電波強度を取得する

Last updated at Posted at 2019-07-08

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で確認してみたところ、こちらは想定通りの結果を得ることができました:ok_woman:

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が返されました・・・:no_good:

原因

一行一行デバッグしてみたところ、6行目の if let statusBarMorden = NSClassFromString("UIStatusBar_Modern"), statusBar .isKind(of: statusBarMorden) { return nil } の部分でreturnされていることがわかりました。

ホームボタンのない機種は UIStatusBar_Modern が使われていて、それ以外の機種は UIStatusBar_Modern が使われていないのだと勝手解釈していたのですが、違ったみたい。。
この辺イマイチわかっていないので、詳しい方教えて欲しいです。:bow:

色々ためしてみる

iPadでも UIStatusBar_Modern が取れるのなら、実がiPhoneX以降用のやり方で電波強度を取得できるのかなと思い、先ほど記載したiPhoneX以降用の関数を呼んでみました。

結果は・・・ダメでした:sob:

どうやらStatusbarの階層構造が違うようです。

iPhoneXSだと、以下のように foregroundView > subview > subview の階層に _UIStatusBarWifiSignalView がいるのに対し、

foregroundView.subviewsの中身
<UIView: 0x1066024c0; frame = (13.3333 14.6667; 66.6667 13.6667); layer = <CALayer: 0x280b6cbe0>>
foregroundView.subviews.subviewsの中身
<_UIStatusBarWifiSignalView: 0x105602b00; frame = (22 2.66667; 15.3333 11); userInteractionEnabled = NO; layer = <CALayer: 0x280b660a0>>

iPadだと、以下のように foregroundView > subview の階層に _UIStatusBarWifiSignalView がいました。

foregroundView.subviewsの中身
<_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段階での数値が必要だったため、ここでもケース分けしなければならず、かなり汚いソースになってしまいました:fearful:

一応想定通り動いているとはいえ、なんとなくスッキリしないので、もっと良い方法ご存知の方いらっしゃればぜひご教示いただきたいです:bow:

2019/11/25 追記

上述の方法はXcode11ではビルドエラーが発生するようになりました。
そのため「statusBarManager」を使用したやり方に修正する必要があります。
私は以下のように記載し、とりあえずは意図した通りに動いておりますが、
かなり手探りであまり自信がないので、もし間違い等あればご指摘ください。:bow:

// 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のwifiEntrycellularEntryにするだけでした。

let cellularEntry = (currentData as AnyObject).value(forKeyPath: "cellularEntry")

ご参考まで。。。:feet:

※ XCode(11.2.1)で作成、iPhoneXS(13.2.2)で動作確認済みです。

参考サイト

8
10
3

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
8
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?