20190708のSwiftに関する記事は4件です。

Swift:文字列を複数の区切り文字で分割する

CharacterSetをうまく使うといい感じにできる.
例えばカンマとピリオドを含んだ文をそれらで分割する場合は以下のようにする.

let str = "a,b,cde,fgh.ijk.lmn"
let list = str.components(separatedBy: CharacterSet(charactersIn: ",."))
Swift.print(list)

// -> ["a", "b", "cde", "fgh", "ijk", "lmn"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

lazyプロパティ内でstaticメソッドを使用すると、2回目以降のアクセスでも処理が走る場合がある

2019/07/09 追記

@nukka123 さんのコメントより、Swiftのlazyプロパティという仕様どんな処理を呼ぼうが異なるスレッドからアクセスすれば初期化処理が1回に止まる保証はないことが判明してます。

https://qiita.com/m-yamada1992/items/d77adc52b75f6d44c3f6#comment-f24b8fc4241d8b918ce3

実行環境

Xcode 10.2.1
Swift 5

ViewController.swift
class ViewController: UIViewController {

    private lazy var label: UILabel = {
        return ViewController.createLabel()
    }()

    init() {
        super.init(nibName: nil, bundle: nil)
        self.addSubview(label)
    }

    func setup(text: String) {
        label.text = text
    }

    static func createLabel() {
        let label = UILabel()
        return label
    }
}

init() で初回のアクセスをしたタイミングでlabelプロパティが生成され、2回目以降にsetup(text:)メソッドでアクセスした際には生成済みのインスタンスにアクセスされることを想定していた。
が、ViewController.createLabel()が2回目以降も走っていることが発覚。

解決策

lazyプロパティ内の定義をいずれかに変更

要は、staticメソッドさえ使わなければ想定通りの遅延初期化プロパティな挙動をする。

直接生成

    private lazy var label: UILabel = {
        let label = UILabel()
        return label
    }()

インスタンスメソッドを用いて生成

    private lazy var label: UILabel = {
        return self.createLabel()
    }()

    static func createLabel() {
        let label = UILabel()
        return label
    }

lazyではなくlet宣言にする

    private let label: UILabel

    init() {
        self.label = ViewController.createLabel()

        super.init(nibName: nil, bundle: nil)
        self.addSubview(label)
    }

    static func createLabel() {
        let label = UILabel()
        return label
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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:

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITableViewCell内のパーツに自身のIndexPathをわからせる手段たち

こんにちは。インターネットねこです。
最近はSwiftUIが話題ですが、あんまりそっちを追えていないので今回は表題に関する内容を書き残しておきます。散々こすられたネタかもしれませんが...

概要

ある程度複雑なTableViewを作成しようと思った時に、セル内のパーツ(UIButton, UISwitch, UITextField...)がIndexPathの情報を必要とする場合が出てきます。その対応策をこの記事では

  1. tagを使う
  2. カスタムクラスを作る
  3. extensionを使う
  4. パーツの座標からIndexPathを求める

の4つのパターンに分けて紹介していきたいと思います。個人的には 4. パーツの座標からIndexPathを求める を推していますが、ご自身の用途に適した方法を採用してください。

今回は、例として以下の画像のようなセルを使用します。対応するクラスも一応載せておきます。イメージとしては、ボタンを押した時に行う処理でIndexPathを必要とするような状況を想定しています。
スクリーンショット 2019-06-21 12.26.30.png

class ButtonCell: UITableViewCell {
    @IBOutlet weak var button: UIButton!
}

1. tagを使う

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCell", for: indexPath)
    cell.button.tag = indexPath.row
    return cell
}

これは単純な方法で簡単に使えますが、複雑なテーブルビューにはオススメできません。複数セクションに対応させるのが面倒だという点と、可読性や保守性を損なうという点があるからです。

まず、上記のコードは、IndexPathのrowしか記録していないため、複数のセクションには対応していません。もしtagを使ってセクションも記録したいのであれば、indexPath.row の代わりにindexPath.section*1000 + indexPath.rowなどと記述してあげればsectionもわかるようになります。もちろん、記録したindexPathを使う場面になったらsectionとrowに分解する処理をいれてあげないといけません。これはイケてなさそうですね。

また、可読性, 保守性の観点からも良くなさそうです。tagがIndexPathを表しているということは、実装した本人しかわかりませんし、tagって名前も微妙です。結論として、tagを使用する方法はあまり良い手ではないように思います。まあ、サクっと作れるアプリなら全然よさそうですけどね。特に1セクション限定でセルの数も少ない場面なら十分有効な手段だと思います。

2. カスタムクラスを作る

final class CustomButton: UIButton {
    var indexPath: IndexPath?
}

tagがダメなら専用のプロパティをもたせればいいじゃない!ということで、カスタムクラスを作ってみます。IndexPathを記録しておくプロパティさえあれば、「1. tagを使う」で出た2つの問題である、複数セクションと可読性問題を解決できそうです。この方法を使う場合は、例で使用しているCellを以下のように書き換えて、

class ButtonCell: UITableViewCell {
    @IBOutlet weak var button: CustomButton!
}

tableView(_:cellForRowAt:)を以下のように書いてあげれば良さそうです。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonCell", for: indexPath) as! ButtonCell
    cell.button.indexPath = indexPath
    return cell
}

この方法で無事に目的を果たせたような気もしますが、たかがIndexPathを保存するためにクラスを作るのは過剰な気もしますよね?しませんか?
僕はしますので、今度はextensionを使ってみます!

3. extensionを使う

extensionでプロパティを追加するのは黒魔術的要素を含んでいるので、あんまり良い手段とは思いませんが、せっかく調べたので一応載せておきます。

var IndexPathKey: Int = 0

extension UIButton {
    var indexPath: IndexPath? {
        get {
            guard let object = objc_getAssociatedObject(self, &IndexPathKey) as? IndexPath else {
                return nil
            }
            return object
        }

        set {
            objc_setAssociatedObject(self, &IndexPathKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

extensionでプロパティ(的なもの)を追加しようとすると上記のようなコードになってしまい黒魔術感が凄いですが、使うときは UIButton.indexPathを使えばいいのでシンプルですね。この黒魔術も記事のテーマを解決する有効な手段だと思います。

4. パーツの座標からIndexPathを求める

これまでに3つの手段を紹介してきましたが、もっとシンプルな方法がありました。それが、パーツの座標からIndexPathを求める方法です。

extension UITableView {
    func indexPath(of view: UIView) -> IndexPath? {
        let location = view.convert(CGPoint.zero, to: self)
        return indexPathForRow(at: location)
    }
}

今回はセル側ではなく、テーブルビュー側を拡張しています。たったこれだけでテーブルビュー側から特定のパーツのIndexPathを取れるようになります。IndexPathが欲しいときは以下のように書いてあげればよいです。

tableView.indexPath(of: button) // tableView内でのbuttonのIndexPathを取得

この方法は、1~3と違ってパーツ自体がIndexPathの情報を持っているわけではなく、テーブルビュー側に問い合わせる形になっているので、テーマとは少し意味合いが変わっていますが、結構シンプルに書けて個人的には気に入っています。問い合わせ先のテーブルビューのインスタンスを知っている前提で書いていますがご了承ください。

とまあ、以上4つ紹介してみました。自分に合ったやり方でやってもらえればいいとおもいます。何か他にもいい方法をご存知の方は教えてください!(文章書くの疲れて段々投げやりになってきてしまうヤツ)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む