デバッグに時間を使いすぎています。誰でもそうです。しかしXcodeには、ほとんどの開発者がほとんど触れない驚くほど強力なツールがあります。これは私が長年かけて構築してきたデバッグワークフロー—問題が発生したときに実際に時間を節約できるものです。
ブレークポイントはあなたが思っているより強力#
ほとんどの人はガッターをクリックしてブレークポイントを設定してそれだけです。しかし右クリックすると、デバッグ方法を変えるたくさんのオプションが見つかります。
条件付きブレークポイント#
1000個のアイテムを処理するループがあるとしましょう。でも347番目のアイテムだけが問題を引き起こしています。346回もブレークポイントにヒットしたくないですよね。
for i in 0..<users.count {
processUser(users[i]) // ここにブレークポイントを設定、条件: i == 347
}ブレークポイントを右クリックして、条件 i == 347 を追加すると、Xcodeはそれが真の場合のみ停止します。userId == "abc123" や error != nil のようなものでも同じように機能します。
シンボリックブレークポイント#
メソッドがどこで呼び出されているかわからないときに便利です。ブレークポイントナビゲータ(Cmd+8)に移動し、+ボタンをクリックして「Symbolic Breakpoint」を選択します。
すべてのビューコントローラがロードされるたびに見たいですか?シンボルを viewDidLoad に設定します。Xcodeはすべての場所で一時停止します。うるさすぎる?絞り込みましょう:MyViewController.viewDidLoad。
私が常備している最も便利なもの:
UIViewAlertForUnsatisfiableConstraints- Auto Layoutの競合を即座にキャッチobjc_exception_throw- クラッシュ前にObjective-C例外で停止malloc_error_break- メモリ割り当ての問題を発見
例外ブレークポイント#
これは新しいプロジェクトで最初にすべきことでしょう。例外ブレークポイントを追加(ブレークポイントナビゲータで + → Exception Breakpoint)すると、Xcodeはアプリがクラッシュした後ではなく、何かがスローされた瞬間に一時停止します。
これでコンソールで謎のクラッシュとしてしか表示されないnil unwrappingの問題を山ほどキャッチできました。
ブレークポイントアクション#
ここからが面白いところです。ブレークポイントは実行を停止する必要はありません—単にログを記録したり、音を鳴らしたりできます。
ブレークポイントを編集し、「Add Action」をクリックすると:
- 停止せずに変数を出力:
"User count: @(users.count)@" - LLDBコマンドを自動実行
- シェルスクリプトを実行
- 音を再生(レアなコードパスに使っています)
「Automatically continue after evaluating actions」にチェックを入れると、ブレークポイントがフローを中断しないロガーになります。
LLDB:実際に使うコンソールコマンド#
ブレークポイントで停止したとき、下部のコンソールはクラッシュログを見るだけのものではありません。それはLLDBで、めちゃくちゃ便利です。
基本#
(lldb) po user
▿ User
- id: "123"
- name: "John Doe"
- email: "john@example.com"po はオブジェクトを読みやすい形式で出力します。使うコマンドの90%はこれです。
より詳細が必要?代わりに p を試してください:
(lldb) p user.name
(String) $R0 = "John Doe"すべてのローカル変数を一度に見る:
(lldb) frame variable実行時に変更する#
これが本当のパワームーブです。再コンパイルせずに変数を変更できます:
(lldb) expr user.name = "Jane Doe"
(lldb) expr index = 0バグを見つけてリビルドせずに修正をテストしたい?変数を変更して続行するだけです。エッジケースを追跡するときに、さまざまな値をテストするのに使ってきました。
メソッドを呼び出すこともできます:
(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)ナビゲーション#
(lldb) bt # コールスタックを表示
(lldb) frame select 3 # 別のスタックフレームにジャンプ
(lldb) continue # 実行を続ける(または単に 'c')
(lldb) n # ステップオーバー(次の行)
(lldb) s # 関数にステップインウォッチポイント#
変数がいつ変更されるか知りたいですか?ウォッチポイントを設定します:
(lldb) watchpoint set variable user.isLoggedInこれで、コードのどこから変更されても実行が一時停止します。予期しない状態変更を追跡するのに最高です。
Console.app:隠れたデバッグツール#
Xcodeのコンソールは悪くないですが、すべてを見る必要がある場合—システムログ、クラッシュレポート、全部—Console.appを開きましょう。
OSLogを使用している場合(使うべきです)、Console.appはそれらのログを検索可能でフィルタリング可能にします:
import os.log
let logger = Logger(subsystem: "com.example.app", category: "networking")
logger.info("Starting API request")
logger.debug("Request URL: \(url.absoluteString)")
logger.error("Failed to decode: \(error.localizedDescription)")Console.appで:
- シミュレータまたは接続されたデバイスを選択
- サブシステムでフィルタリング:
subsystem:com.example.app - レベルでフィルタリング:
subsystem:com.example.app AND level:error
よく使うデバッグセッション用にフィルタ述語を保存できます。ネットワークリクエスト用、データベース操作用、エラーと警告だけを表示する用のものを持っています。
print()よりOSLogを使う理由#
OSLogは print() をあちこちに散りばめるよりもはるかに優れています:
- 構造化されている—レベルとカテゴリでフィルタリングできます
- 高速—ログは最適化されておりアプリを遅くしません
- 本番環境で機密データを自動的に編集します
- アプリがクラッシュした後もログが永続化されます
クイックデバッグには print() を。後で見たいものにはOSLogを使いましょう。
高度なConsole.appフィルタリング#
Console.appは強力な述語クエリをサポートしています。定期的に使うもの:
# アプリ内のすべてのエラーと障害
subsystem:com.example.app AND (level:error OR level:fault)
# 失敗したネットワークリクエスト
subsystem:com.example.app AND category:networking AND eventMessage CONTAINS "failed"
# 過去5分間のすべて
subsystem:com.example.app AND timestamp >= now(-5m)ログをエクスポートしてチームと共有したり、バグレポートに添付したりすることもできます。Xcodeのコンソールからコピペしようとするよりもはるかに便利です。
ビューデバッグ#
UIが壊れていて理由がわからないとき、ビューデバッガーは救世主です。
アプリを実行し、壊れた画面に移動して、Xcodeのデバッグバーの「Debug View Hierarchy」ボタンをクリック(またはCmd+Shift+D)。階層のすべてのビューの3D展開ビューが表示されます。
回転させると見えてきます:
- 他のビューの背後に隠れてレンダリングされているビュー
- 画面外にあるビュー
- サイズがゼロのビュー
- 選択したビューの完全な制約チェーン
タップイベントをブロックしている見えないボタン、0x0でレンダリングされているラベル、誤って10,000ピクセル幅になっている画像を見つけるのに使いました。
Auto Layoutデバッグ#
ビュー階層の紫色の警告アイコンは制約の競合を意味します。クリックすると、Xcodeは正確にどの制約が競合しているかを表示します。
プロのヒント:制約に識別子を付けましょう:
heightConstraint.identifier = "ProfileImageHeight"制約が壊れると、メモリアドレスの代わりに "ProfileImageHeight" がエラーに表示されます。デバッグがはるかに楽になります。
LLDBから制約ツリーを出力することもできます:
(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()時間を節約するシミュレータ機能#
スローアニメーション#
Debug → Slow Animationsはすべてを1/10の速度に。トランジション中に何が起こっているか確認したり、アニメーションがおかしく見える理由を理解するのに最適です。
メモリ警告のシミュレート#
Debug → Simulate Memory Warning。アプリが低メモリをどう処理するかテストします。この方法で多くの画像キャッシュバグを見つけました—メモリ警告をトリガーするまでは正常に動作するのに、突然すべての画像が消えるやつです。
Network Link Conditioner#
Xcode → Open Developer Tools → Network Link Conditioner
3G、LTE、高いパケット損失、完全なオフラインモードをシミュレートします。APIリクエストがWiFiでは正常なのにセルラーでタイムアウトした経験があるなら、これが原因です。
500msの遅延と10%のパケット損失の「Bad Network」プロファイルを保持しています。高速オフィスWiFiでは決して見られないタイムアウトバグやローディング状態の問題をキャッチします。
ステータスバーのオーバーライド#
シミュレータのステータスバーを右クリックすると、時間、バッテリーレベル、信号強度、キャリアをオーバーライドできます。一貫したスクリーンショットや、さまざまなバッテリーレベルでのUI確認に便利。
位置情報シミュレーション#
Debug → Locationで、デスクを離れることなくさまざまな場所をシミュレート。カスタムロケーション、都市散歩、高速道路走行—すべて利用可能。位置ベースの機能テストや、特定地域でのみ発生する問題のデバッグに超便利です。
メモリデバッグ#
Debug Memory Graph#
Debug Memory Graphボタンをクリック(Cmd+Shift+M)すると、Xcodeはメモリ内のすべてのオブジェクト、その関係、そして重要なのはどれがリークしているかを表示します。
紫色の感嘆符はリーク。クリックすると保持サイクルが見えます。
一番よく見るリーク:
class ViewController: UIViewController {
var onComplete: (() -> Void)?
func setupHandler() {
onComplete = {
self.dismiss(animated: true) // ❌ selfを強く参照
}
}
}weak selfで修正:
onComplete = { [weak self] in
self?.dismiss(animated: true) // ✓ 保持サイクルなし
}デリゲートもweakにする必要があります:
weak var delegate: ManagerDelegate? // 単なる 'var' ではなくInstruments#
本格的なメモリ調査には、Instrumentsを使います(Product → Profile、またはCmd+I)。
Allocationsインストゥルメントが表示するもの:
- すべてのオブジェクト割り当て
- 時間経過に伴うメモリ増加
- 最もメモリを使用しているクラス
- 割り当てが発生した場所を示すスタックトレース
通常、探すもの:
- 時間とともに線形に増加するメモリ(おそらくリーク)
- 予期しない大きな割り当て(1000枚の画像を一度にロードしてしまった?)
- 解放されるべきなのに解放されていないオブジェクト
アクション前後に世代をマーク(小さな旗ボタン)して、何が不必要に永続化されているか確認しましょう。
パフォーマンスプロファイリング#
Time Profiler#
Product → Profile → Time Profiler。アプリで遅い操作をしながら記録し、コールツリーを見ます。
「Self Weight」でソートしてボトルネックを見つけます。関数が40%のself weightを示していたら、そこを最適化すべきです。
JSON解析関数が毎秒1000回呼ばれていたことを見つけたことがあります。バックグラウンドキューに移動したら、UIの引っかかりが止まりました。
Main Thread Checker#
XcodeはバックグラウンドスレッドでのUI更新を自動的にキャッチします。これが表示されたら:
Main Thread Checker: UI API called on a background thread: -[UILabel setText:]こういうことをしてしまっています:
URLSession.shared.dataTask(with: url) { data, response, error in
self.label.text = "Loaded" // ❌ クラッシュ!
}修正:
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
self.label.text = "Loaded" // ✓
}
}ランタイム診断#
Edit Scheme → Run → Diagnostics。手動では決して見つけられないバグをキャッチするチェックボックスがたくさんあります。
Address Sanitizer#
メモリ破損を発見:
- Use-after-freeエラー
- バッファオーバーフロー
- メモリリーク
アプリが2〜3倍遅くなりますが、他では追跡不可能なバグをキャッチします。時々しか発生しないクラッシュを追うときにオンにします。
Thread Sanitizer#
データ競合とスレッドの問題をキャッチ。これを有効にしてアプリを実行し、通常どおり操作します。競合状態があれば、Thread Sanitizerが見つけます。
Address SanitizerとThread Sanitizerを同時に実行できないので、奇妙なクラッシュのデバッグ時には交互に使います。
Zombie Objects#
解放されたオブジェクトへのメッセージをキャッチ。有効にすると、解放済みメモリへのアクセス時にXcodeが正確に教えてくれます:
*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000これはオンにしたままにしないでください—オブジェクトが解放されなくなるのでメモリ使用量が増加します。でも特定のuse-after-freeバグを見つけるには最適です。
よくあるデバッグシナリオ#
起動時にクラッシュ#
まず例外ブレークポイントを追加。大抵はそれでキャッチできます。
ダメなら確認:
- Info.plistのキー不足
- 初期化コードでの強制アンラップ
- 依存性注入の失敗
UIが更新されない#
毎回。毎回。次のどれかです:
- 更新がメインスレッドにない
- アウトレットが接続されていない
- ビューが実際に表示されていない(LLDBで
po view.windowを確認—nilなら階層にいません)
テーブル/コレクションビューなら、reloadData() の呼び忘れです。
メモリ警告でクラッシュ#
Instruments Allocationsで何がメモリを使っているか確認。大抵は:
- キャッシュから解放されない画像
- 解放されないビューコントローラ(保持サイクル)
- 永遠に成長する配列や辞書
シミュレータでメモリ警告をシミュレートしてクリーンアップコードをテストしましょう。
謎のクラッシュ#
全サニタイザーを有効に。断続的ならおそらくスレッド—Thread Sanitizerで実行。システムフレームワーク内ならおそらくメモリ破損—Address Sanitizerで実行。
シミュレータでのOTA更新デバッグ#
シミュレータはover-the-air更新フローのデバッグに最適です。やっていること:
ネットワークトラフィックを監視。 アプリのサブシステムにフィルタリングしたConsole.appで、マニフェストのリクエストとレスポンスをリアルタイムで確認。送信ヘッダーとサーバーの返答が正確に見えます。
悪いネットワークをシミュレート。 Network Link Conditionerで、更新フローが遅い接続やパケット損失をどう処理するかテスト。ハングする?ローディング表示される?優雅にフォールバックする?
バンドルのロードを確認。 更新チェックロジックの周りにログを追加。更新チェック時、更新利用可能時、ダウンロード開始時、新バンドルロード時にログを取ります。
import os.log
let logger = Logger(subsystem: "com.example.app", category: "updates")
logger.info("Checking for updates...")
let update = try await Updates.checkForUpdateAsync()
logger.info("Update available: \(update.isAvailable)")マニフェストエンドポイントをテスト。 アプリが送るのと同じヘッダーで手動curlできます:
curl -H "expo-protocol-version: 1" \
-H "expo-platform: ios" \
-H "expo-runtime-version: 1.0.0" \
https://your-server.com/api/expo-updates/manifest/クライアント側をデバッグする前に、サーバーが正しいデータを返しているか確認できます。
サードパーティツール#
Charles Proxy / Proxyman#
全ネットワークトラフィックを確認。リクエストとレスポンスの変更。エラー条件のテスト。APIデバッグに不可欠。
ProxymanはCharlesより良いUIで、macOSでよりネイティブに感じます。去年切り替えてから戻っていません。
Reveal#
Xcodeのビューデバッガーに似ていますがより強力。Xcodeに接続せずに物理デバイスで動作。テスターデバイスのレイアウト問題デバッグに最適。
実機でのデバッグ#
ワイヤレスデバッグ#
USBでデバイスを一度接続、Window → Devices and Simulatorsで「Connect via network」にチェック、プラグを抜く。同じWiFi上にいる限り、デバイスはXcodeに残ります。
Device Console#
Window → Devices and Simulators → Open Console。すべてが見えます:システムログ、アプリログ、クラッシュレポート。Xcodeコンソールよりはるかに詳細。
テスターが「アプリがクラッシュした」と言ったら、いつもデバイスを接続してコンソールログを取得するよう頼みます。大抵すべてを説明するシステムレベルのエラーがあります。
実際にデバッグするときにやること#
- 例外ブレークポイントを追加(なければ)
- バグを再現
- ブレークポイントを設定(壊れていそうな場所の近く)
- LLDBで
poを使って値を確認 - 変数を変更して再コンパイルせずに修正テスト
- ビュー階層を確認(UI関連なら)
- Instrumentsでプロファイル(パフォーマンス関連なら)
- サニタイザーをオン(メモリ・スレッド関連なら)
重要なのは症状から逆算すること。クラッシュ?例外ブレークポイント。遅い?Time Profiler。メモリ?Instruments。変なUI?ビューデバッガー。
もっと早く知りたかったヒント#
アサーションを使う。 ロジックエラーがバグになる前にキャッチ:
assert(users.count > 0, "Users array should never be empty here")デバッグ前にコミット。 理論テストのために変更し始めると、元に戻したくなります。
printを消す。 少なくとも #if DEBUG でラップ。古いデバッグログは新しい問題のデバッグを難しくします。
LLDBを学ぶ。 アプリを20回リビルドするより速いです。
シミュレータは本物じゃない。 デバイスでのみ出るバグは、常にスレッドかメモリの問題。もしくはシミュレータがiPhoneチップではなくMacのCPUを使うため、パフォーマンス関連です。
開いているツール#
- Xcode(当然)
- アプリのサブシステムにフィルタリングしたConsole.app
- ネットワークデバッグ用Proxyman
- 深刻な場合はInstruments
デバッグはすべてのツールを知ることじゃありません—特定の症状を見たときにどのツールを掴むかを知ることです。ビューデバッガーはメモリリークには役立たないし、InstrumentsはAuto Layoutの競合には役立ちません。
デバッグするほど、パターンの認識が速くなります。「これは間違いなく保持サイクルだ。」「メインスレッド違反っぽい。」「おそらく制約の優先順位の問題だ。」
その直感を築けば、デバッグはずっと楽になります。

