SwiftUIやPlaygroundもいいけどUIのデバッグぐらいLLDBでやっちゃおうぜ

PlaygroundやSwiftUIの登場によってだいぶUIのデバッグがしやすくなってきたと思うけど、
Playgroundはいちいちそれ用のファイル開かないといけないし、SwiftUIはまだXcode11がBeta版なので使う機会が少ない。
UIの確認をするために一々ビルドするのも気が重いのでそんなときはビルドなしでUIの変更が確認できるLLDBの出番ですよと。
LLDBはXcodeのデバッガーなのでUI以外のデバッグにも当然使えるが、今回はUIに絞ったものを紹介していく。

環境

Xcode10.2

LLDB初めの一歩〜ブレークポイントを貼る〜

これ、LLDBを解説してる記事とか見ると当然のごとく書いてあるけど、最初のうちは一体どこにブレークポイント設置したらいいのかわからない。 そこでおすすめなのがviewDidAppear()にブレークポイントを設置する方法だ。

実際こんなViewControllerがあるとする。

import UIKit

class ViewController: UIViewController {

    let myView: UIView!
    let myLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        myView = UIView()
        myView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        myView.center = self.view.center
        myView.backgroundColor = .cyan
        self.view.addSubview(myView)

        myLabel = UILabel()
        myLabel.frame = CGRect(x: 100, y: 100, width: 200, height: 50)
        myLabel.backgroundColor = .green
        myLabel.text = "this is label"
        self.view.addSubview(myLabel)
    }
}

初期のViewControllerはviewDidLoad()しか宣言されていないと思うので下記を追加する。

import UIKit

class ViewController: UIViewController {

    let myView: UIView!
    let myLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        myView = UIView()
        (中略)
        self.view.addSubview(myView)
    }

    /**
      * こいつを追加する
      */
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
}

そんでもってブレークポイントはviewDidAppear()の一番最後。実行がし終わったところに貼る。

f:id:rasukarusan:20190816214402p:plain
viewDidAppear()の閉じカッコにブレークポイントを貼る

というのもUIViewControllerのライフサイクル的に、レイアウトの描画が完全に終了するのがviewDidAppear()なのでUIをLLDBで変更したいときはここがベスト。ライフサイクルについては下の記事がめちゃめちゃ参考になる。

qiita.com

UIを変更する

ということで早速LLDBでUIに変更を加えてみる。 ブレークポイントを貼った状態でRunすると下記のように止まると思う。

f:id:rasukarusan:20190816214455p:plain
RunするとViewが描かれた後止まる

この状態でLLDBにコマンドを打ち込んでUIの変更をしていく。とりあえず背景色の変更をしてみる。

(lldb) expression myView.backgroundColor = .blue

これで変更はOK。再描画するために以下のコマンドを打つ。

(lldb) expression CATransaction.flush()

シミュレータを見てみると色が変わっているはず。実際の動作は下のような感じ。

https://rasukarusan.github.io/blog-assets/xcode-lldb/first_demo.gif

viewの背景色を変更

Labelのテキストを変更してみたり、

https://rasukarusan.github.io/blog-assets/xcode-lldb/label_demo.gif
labelのテキストを変更

邪魔だなと思う要素を非表示にしたりできる。

https://rasukarusan.github.io/blog-assets/xcode-lldb/hidden_demo.gif
要素を非表示にする

基本的にはこんな感じでUI変更したらflush()で再描画、という流れ。

privateなプロパティを操作する

今まではグローバルにセットされたViewを操作していたが、下記のようにviewDidLoad()内で追加した要素を操作したい場合、今までのやり方では操作できない。

import UIKit

class ViewController: UIViewController {

    let myView = UIView() // ← グローバル

    override func viewDidLoad() {
        super.viewDidLoad()

        myView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        myView.center = self.view.center
        myView.backgroundColor = .cyan
        self.view.addSubview(myView)

        let myLabel = UILabel() // ← private
        myLabel.frame = CGRect(x: 100, y: 100, width: 200, height: 50)
        myLabel.backgroundColor = .green
        myLabel.text = "this is label"
        self.view.addSubview(myLabel)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
}

この状態でlet myLabel = UILabel()で定義されたUILabelを操作したい場合、参照できない。

(lldb) expression myLabel.backgroundcolor = .blue

error: <EXPR>:3:1: error: use of unresolved identifier 'myLabel'; did you mean 'UILabel'?
myLabel.backgroundcolor = .blue
^~~~~~~
UILabel

いわゆるこういったprivateなプロパティにアクセスするにはアドレスを直接指定する。

// アドレスを直接指定。to:にはその要素の「クラス名.self」を指定する
(lldb) expression unsafeBitCast(0x7fa1e8d0aad0,to:UILabel.self).backgroundColor = .green

// 毎回unsafeBitCast(...)と書くのは面倒なのでletで定義してあげるといい感じ
(lldb) expression let $lbl = unsafeBitCast(0x7fa1e8d0aad0,to:UILabel.self)
(lldb) expression $lbl.backgroundColor = .green

アドレスの取得方法については色々あるけどrecursiveDescriptionで取得するのが楽。

(lldb) po self.view.value(forKey: "recursiveDescription")!

<UIView: 0x7fa1e8d0ae00; frame = (0 0; 414 896); autoresize = W+H; layer = <CALayer: 0x6000003f1a40>>
   | <UIView: 0x7fa1e8c1bde0; frame = (157 398; 100 100); layer = <CALayer: 0x6000003c47e0>>
   | <UILabel: 0x7fa1e8d0aad0; frame = (100 100; 200 50); text = 'this is label'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000020819f0>>

もしくは、GUIの操作になってしまうが「Debug View Hierarchy」を使うのも悪くない。

f:id:rasukarusan:20190816215223p:plain

LLDBのTips

コマンドは省略できる

UIを変更する際、毎回expressionと打つのはだるすぎる。 LLDBの標準機能として、expressionには省略コマンドが用意されている。以下のコマンドはすべて同じ結果になる。

(lldb) expression myView.backgroundColor = .red
(lldb) expr myView.backgroundColor = .red
(lldb) e myView.backgroundColor = .red

また、再描画の度にexpression CATransaction.flush()と打つのも中々億劫なので、そういう場合はaliasが設定できる。

// CATransaction.flush()を'cl'にする
(lldb) command alias cl expression CATransaction.flush()

// 実際に使う時
(lldb) e myView.backgroundColor = .red
(lldb) cl // エイリアスコマンドで再描画

終わり

0→1でUIを作っていく時はPlayground、ある程度UIが固まってきて細かい調整をするときはLLDB使っていくみたいなのが割といい感じ。何良りLLDBは描画一瞬で終わるのが良い。PlaygroundではPCの性能にもよるだろうが、結構待たないといけないので中々鬱陶しい。
いち早く、かつ割と手軽にUIを変更したいときにLLDBを使うのは結構おすすめだと思う。