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

SwiftでWifiの電波強度を取得する方法です。
検索して調べたやり方で実装してみたところ、私の手元の環境ではうまく動きませんでした。
試行錯誤の末、自分なりに辿り着いた解決方法を記載します。


# 動作確認環境

|機種|iOSのバージョン|
|:---|:---:|
|iPhone XS|12.3.1|
|iPad 第５世代|12.1.4|

# WiFi情報はどこから取得するのか
参考サイトによると、ステータスバーの中にSignalに関する情報が入っているようです。
また、iPhoneX以降とそれ以前とではステータスバーの取得方法が異なるようです。

# まずは試してみる
参考サイトのコードを真似して実装してみました。

## iPhoneX以降用
```swift
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以前用
```swift
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:

どうやらStatusbarの階層構造が違うようです。

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` がある場合とない場合とで処理を分けてみました。

```swift
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:

```swift
// 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")
```

ご参考まで。。。:feet:

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

# 参考サイト

- https://github.com/zhihuitang/WiFiSignal
- https://blog.csdn.net/JWheat/article/details/102466750 (2019/11/25追記)