Think Big Act Local

iPhone開発を軸にブレブレの記事を書いていきます。

初級者から中級者にレベルアップするためのXcodeデバッグ術

効率よくiOSアプリ開発を行うために、効率よくデバッグを行いたいですよね。

このエントリでは「print文を書く以外デバッグの方法を知らなかったあの頃の自分」を初級者と定義して、自分がやってるデバッグ方法について書いてみます。

Xcodeデバッグ

1. printを使わずに変数の中身を確認する

age, name, coverImage という以下の3つの変数が宣言されています。

let age = 27
let name = "Ryosuke Hiramatsu"
let coverImage = UIImage(named: "sample.jpg")

これらの変数の中身をチェックしたい時、printで出力するのでも良いですが、それでは出力する値を変えたくなった時(print(age)print(age*2+1)に変更とか)に再度ビルドが必要になって時間がかかります。

printではなく、LLDBコマンドを使って確認することができます。

f:id:himaratsu:20160325214853p:plain

こんな感じで Breakpoint を貼って*1、そこで処理を止めた状態で Console に以下のように入力します。

(lldb) po age
27

(lldb) po name
"Ryosuke Hiramatsu"

単純に変数の値を出力するだけではなく、式の評価もできます。

(lldb) po age+1
28

(lldb) po name.characters.split(" ").map{ String($0) }.first
"Ryosuke"

2. QuickLookを使ってUIImageの中身を確認する

coverImage 変数に対して、printやpoコマンドではどんな画像なのか確認できません。

let coverImage = UIImage(named: "sample.jpg")
// print(coverImage) とした時のコンソール出力
Optional(<UIImage: 0x14fe19460>, {640, 640})

こういった場合、変数の上にマウスカーソルを合わせ、

f:id:himaratsu:20160325215318p:plain

現れたポップアップの目のアイコンをタップすることで画像が確認できます。

f:id:himaratsu:20160325215428p:plain

変数の値や画像の表示は、Xcodeの左下のAreaからも可能

変数の確認や QuickLook による画像の確認は Console の左のエリアからも可能です。

age や name などの変数名の横に値が出ていて一目で確認できます。

f:id:himaratsu:20160325215632p:plain

coverImage を選択した状態で目のアイコンをクリックして画像を表示することも可能です。

f:id:himaratsu:20160325215706p:plain

単純な変数の確認であればこちらの方が便利ですね。

3. 自作クラスのdescriptionをカスタマイズする

例えば以下のようなクラスを作ったとします。

class Person {
    let name: String
    let age: Int
    let coverImage: UIImage?
    
    init(name: String, age: Int, coverImage: UIImage?) {
        self.name = name
        self.age = age
        self.coverImage = coverImage
    }
}

以下のようにPersonクラスのインスタンスを生成します。

let person = Person(name: name, age: age, coverImage: coverImage)

このインスタンスのプロパティを確認しようと思ってprint(person)としてみると、Consoleには以下のように表示されます。

// print(person)
XcodeDebugPractice.Person

これは知りたい情報ではありません。LLDBでpo person.nameなどとすれば確認はできますが、少し手間がかかります。
こういうシーンではクラスのログ出力をカスタマイズしましょう。

CustomStringConvertibleを使う

先ほどの Person クラスを編集して、以下のようにします。

class Person: CustomStringConvertible { //★
    let name: String
    let age: Int
    let coverImage: UIImage?
    
    init(name: String, age: Int, coverImage: UIImage?) {
        self.name = name
        self.age = age
        self.coverImage = coverImage
    }
    
    //★
    var description: String {
        return "\(name), \(age)"
    }
}

この状態で再びprint(person)するとコンソールには以下が出力されます。

Ryosuke Hiramatsu, 27

description で指定した値が出ています。CustomStringConvertibleは print などで出力される値をカスタムするために用意されている Protocol です。

さらに、CustomStringDebugConvertible Protocol にも準拠してみます。

class Person: CustomStringConvertible,
    CustomDebugStringConvertible {  // ★
    // ... 省略 ...    
    var description: String {
        return "\(name), \(age)"
    }
    
    var debugDescription: String {  // ★
        return "\(name), \(age), \(coverImage)"
    }
}

CustomStringDebugConvertible はpoコマンドなどでデバッグプリントする際の値をカスタムできるものです。

他にも CustomReflectableCustomLeafReflectableCustomPlaygroundQuickLookable などが用意されていますので興味のある方は調べてみてください。

4. Breakpointをカスタマイズする

Breakpoint をもっと便利に使いましょう。例えば Loop で特定の条件の時だけ止めたかったり、あるいは処理は止めずに Breakpoint を通過したら Console にログを吐きたい、というような場面があるとします。そんな時には Breakpoint のカスタマイズが便利です。

例えばこんな感じのループ文を用意して試してみます。

for i in 0...50 {
    print(i)
}

ブレイクポイントを右クリックして「Edit Breakpoint」を選択し、

f:id:himaratsu:20160325221924p:plain

conditionに条件を入れます。今回は「i==22の時のみ止まる」ように設定してます。

f:id:himaratsu:20160325221955p:plain

この状態で実行すると、i==22のループの時のみ Breakpoint で処理が止まります。特定条件の時に変数の値を確認したい場合などに便利ですね。

おまけ:ブレイク時に音を出してデバッグする

Breakpoint では「ブレイク時にどんなOutputをするか」もカスタム可能です。

例えば「i==22の時だけ変数の値を確認したい。でも処理は止めずに続けたくて、値の内容は音声で確認したい」という場合は以下のように設定します。

f:id:himaratsu:20160325222529p:plain

これで実行すると「22 hit!」という音声が(おそらくsayコマンドで)発声され、下の「Automatically continue after evaluating actions」にチェックをつけているため処理は止まらずに継続されます。

いろんな特殊条件で発生する不具合を調査する際など、Condition をうまく使うことでデバッグを効率的に行うことができます。ちなみに自分は実プロジェクトで音声デバッグを使ったことはありませんw

5. アプリ内に保存したデータを確認する

NSUserDefaults や Document ディレクトリなどに保存したデータの中身を確認する方法です。
例えば以下のコードで値や画像を保存したとします:

// NSUserDefaultsに値を保存
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject("@himara2", forKey: "account")
defaults.synchronize()

// Diskに画像を保存
if let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .AllDomainsMask, true).first,
  coverImage = coverImage {
    let targetPath = path.stringByAppendingString("/sample_data.jpg")
      if let data = UIImageJPEGRepresentation(coverImage, 0.5) {
        let success = data.writeToFile(targetPath, atomically: true)
        print(success)
      }
}

保存されたデータを確認してみます。

Xcode > Window > Organizer と選択して Organizer を開きます。

f:id:himaratsu:20160325223309p:plain

左メニューでアプリを実行したデバイスを選択して、対象のアプリを選択します。

f:id:himaratsu:20160325223343p:plain

設定ボタンから「Download Container...」を選択し、適当な場所に保存します。

f:id:himaratsu:20160325223425p:plain

xcappdata 拡張子のファイルが保存されますので、右クリックして「パッケージの内容を表示」します。

f:id:himaratsu:20160325223607p:plain

中身はこのようになっています。
Documents/ に保存した画像ファイル(sample_data.jpg)が存在しているのが分かります。

f:id:himaratsu:20160325224142p:plain

さらに、Library/Preferences/ 下のplistの中身をみると、

f:id:himaratsu:20160325224304p:plain

このように NSUserDefaults の値が保存されていることが分かります。

保存した内容を取り出すコードを実装するのでも良いのですが、ハマった時は原因が切り分けが大事なので、実際に保存されている情報を目視できるのは意味のあることだと思っています。

まとめ

Xcodeデバッグ手法に関する記事は以外と少なく、自分が最初にアプリ開発を始めた時は NSLog を大量に埋めて頑張ってました。
今回紹介したようなデバッグ手法を知ってからスピーディに開発できるようになったな、と思ったのでこのような記事を書かせていただきました。

何か間違ってる部分や、他に良いデバッグ方法などご存知でしたらぜひ教えてください :)

みなさんのデバッグライフに少しでもお役立ちできれば光栄です。

サンプルコード

github.com

今回引用したコードはすべてこちらでお試しいただけます。

こちらもいかがですか?

himaratsu.hatenablog.com

himaratsu.hatenablog.com

*1:Breakpointは行番号のあたりをクリックで設定/解除できます