<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Debugging · 仁才徳</title>
    <link>https://jared.lynskey.co.nz/ja/tags/debugging/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Tue, 27 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/tags/debugging/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Playwrightの「Test timeout of 30000ms exceeded.」：まず終わらない待機を探す</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-10-22-playwright-timeout-30000ms/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-10-22-playwright-timeout-30000ms/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Playwright</category>
      <category>UI Testing</category>
      <category>Testing Best Practices</category>
      <category>Debugging</category>
      <description>Playwrightの「Test timeout of 30000ms exceeded.」が実際に何を測っているのか、私が確認する順番で見たよくある原因、止まったステップの見つけ方、そしてタイムアウトを延ばすのが間違いになる場合をまとめました。自分のテストスイートで実際に起きた失敗、5つのテストと名前の変わったテストID 1つの話も添えています。</description>
      <content:encoded><![CDATA[<p><code>Test timeout of 30000ms exceeded.</code> は、Playwrightのテストが30秒の持ち時間を超えたという意味です。30秒はPlaywright Testのデフォルトで、この持ち時間にはテスト本体、<code>beforeEach</code> フック、フィクスチャのセットアップがまとめて含まれます。メッセージがステップではなくテストを名指しするのは、クリックや入力、ページ遷移にはデフォルトで専用のタイムアウトがないからです。どれだけかかっても待ち続け、テストタイムアウトはたまたま最初に切れる時計にすぎません。</p>
<p>つまり、このエラーが本当に速度の問題であることはまれです。たいていは、どこかのステップが決して起こらないことを待っています。私が確認する順に、原因と対処を並べるとこうなります。</p>
<ol>
<li>ロケーターが何にも一致せず、<code>click()</code> や <code>fill()</code> がテストの持ち時間が尽きるまで待ち続けている。よくある理由は、テストIDの変更、テキストの変更、手前に被さったダイアログです。ロケーターを直します。</li>
<li>ページ遷移や <code>waitForURL()</code> が、いつまでも来ないページを待っている。リダイレクトやログイン画面、初回起動の画面のせいで、アプリが別の場所へ行ってしまったケースです。待つ対象を直します。</li>
<li>セットアップが本当に遅い。初回リクエストでようやくコンパイルを始める開発サーバー、ゼロスケールから起き上がるバックエンド、<code>beforeEach</code> のたびにUIで行うログインなどです。環境を直すか、セットアップを1回で済ませます。</li>
<li>テストが単純に長い。そのときに限って、<code>test.slow()</code> か <code>test.setTimeout()</code> でそのテストに時間を足します。</li>
</ol>
<p>どれであっても、最初にやることは、時間切れの瞬間にまだ動いていたステップを見つけることです。次にやるのが <code>actionTimeout</code> の設定で、そうしておけば次の失敗ではエラーのほうがステップを教えてくれます。</p>

<h2 class="relative group">30秒に含まれるもの
    <div id="30秒に含まれるもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#30%e7%a7%92%e3%81%ab%e5%90%ab%e3%81%be%e3%82%8c%e3%82%8b%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>テストタイムアウトは、フィクスチャのセットアップ、<code>beforeEach</code> フック、テスト関数が共有する1つの持ち時間です。テストが終わると、<code>afterEach</code> フックとフィクスチャのティアダウンには、同じ長さの持ち時間が別に与えられます。<code>beforeAll</code> と <code>afterAll</code> も同じ値を受け取りますが、どのテストとも時間を共有せず、失敗したときも専用のメッセージを出します。</p>
<p>この持ち時間の中で、自分の時計を持っているのはアサーションだけです。<code>await expect(locator).toBeVisible()</code> はデフォルトで5秒たつと諦め、何を探していたのかを正確に教えてくれます。アクションとページ遷移はそうではありません。<code>actionTimeout</code> と <code>navigationTimeout</code> のデフォルトは無制限です。このエラーがこれほど曖昧な理由は、すべてここにあります。決して現れない要素へのクリックは失敗しません。ほかの何かがテストを終わらせるまで、行儀よく待ち続けるだけです。</p>
<p>実行を終わらせうるタイムアウトをすべて、それぞれが出すメッセージと一緒に並べました。メッセージは、私のプロジェクトに入っている中で最も古い1.58と最も新しい1.63からそのまま写しています。</p>
<table>
	<thead>
			<tr>
					<th>タイムアウト</th>
					<th>デフォルト</th>
					<th>設定する場所</th>
					<th>出力されるメッセージ</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>テスト（本体、<code>beforeEach</code>、フィクスチャのセットアップ）</td>
					<td>30,000 ms</td>
					<td>設定ファイルの <code>timeout</code>、<code>--timeout</code>、<code>test.setTimeout()</code>、<code>test.slow()</code></td>
					<td><code>Test timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td><code>beforeAll</code> / <code>afterAll</code></td>
					<td>テストタイムアウトと同じ値</td>
					<td>フック内で <code>test.setTimeout()</code></td>
					<td><code>&quot;beforeAll&quot; hook timeout of 30000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>アサーション（<code>expect</code>）</td>
					<td>5,000 ms</td>
					<td>設定ファイルの <code>expect: { timeout }</code>、アサーションごとの <code>{ timeout }</code></td>
					<td><code>expect(locator).toBeVisible() failed</code> … <code>Timeout: 5000ms</code></td>
			</tr>
			<tr>
					<td>アクション（<code>click</code>、<code>fill</code>）</td>
					<td>なし</td>
					<td><code>use: { actionTimeout }</code>、アクションごとの <code>{ timeout }</code></td>
					<td><code>TimeoutError: locator.click: Timeout 10000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>ページ遷移（<code>goto</code>、<code>waitForURL</code>）</td>
					<td>なし</td>
					<td><code>use: { navigationTimeout }</code>、呼び出しごとの <code>{ timeout }</code></td>
					<td><code>TimeoutError: page.waitForURL: Timeout 15000ms exceeded.</code></td>
			</tr>
			<tr>
					<td>実行全体</td>
					<td>なし</td>
					<td>設定ファイルの <code>globalTimeout</code></td>
					<td><code>Timed out waiting 3600s for the test suite to run</code></td>
			</tr>
			<tr>
					<td><code>webServer</code> の起動</td>
					<td>60,000 ms</td>
					<td><code>webServer: { timeout }</code></td>
					<td><code>Timed out waiting 60000ms from config.webServer.</code></td>
			</tr>
	</tbody>
</table>
<p>アクション、ページ遷移、実行全体の行にはデフォルト値がないので、表の数字は自分で設定した値が入る場所です。</p>

<h2 class="relative group">5つのテストと、名前の変わったテストID
    <div id="5つのテストと名前の変わったテストid" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5%e3%81%a4%e3%81%ae%e3%83%86%e3%82%b9%e3%83%88%e3%81%a8%e5%90%8d%e5%89%8d%e3%81%ae%e5%a4%89%e3%82%8f%e3%81%a3%e3%81%9f%e3%83%86%e3%82%b9%e3%83%88id" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>2月、私が作っている見積もりアプリのテストスイートで、5つのテストが1回の実行の中でまったく同じように失敗しました。このスイートは、開発サーバーが配信するアプリのWeb版を、ワーカー1つで、シードデータを入れ直したローカルのバックエンドに対して動かしています。5つのうち最初の1つを、少し削って載せます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  1) [default] › tests/templates.spec.ts:50:7 › Templates › should edit a template ───
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Test timeout of 30000ms exceeded.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Error: locator.click: Target page, context or browser has been closed
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      57 |       .locator(&#39;[data-testid=&#34;template-item&#34;]&#39;)
</span></span><span class="line"><span class="cl">      58 |       .filter({ hasText: &#34;E2E Edit Template&#34; });
</span></span><span class="line"><span class="cl">    &gt; 59 |     await item.locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;).click();
</span></span><span class="line"><span class="cl">         |                                                         ^
</span></span><span class="line"><span class="cl">      60 |     await page.waitForTimeout(500);</span></span></code></pre></div></div>
<p><code>Target page, context or browser has been closed</code> の行だけ見ると、ブラウザが落ちたように読めます。違います。足元でページが閉じられたときに、アクションが出すメッセージです。30秒が尽きてPlaywrightがテストを片付け、まだ待っていたクリックが、失ったページのことを報告したわけです。組み込みの <code>page</code> フィクスチャを使っていれば、この行は <code>Error: locator.click: Test timeout of 30000ms exceeded.</code> のようにタイムアウトをもう一度伝えていたはずです。このテストは <code>beforeAll</code> で自前のブラウザコンテキストを開いているので、クリックには汎用のメッセージが届きました。</p>
<p>役に立つのはコールログです。クリックは <code>[data-testid=&quot;template-item&quot;]</code> を待っていました。その10時間前、私は一覧の行コンポーネントのテストIDを <code>template-item</code> から <code>template-item-${name}</code> に変えていました。ネイティブアプリのE2Eテストが、名前で特定の行を選べるようにするためです。<code>=</code> を使った属性セレクターは値全体が一致しないとマッチしないので、Webのテストはページのどこにも存在しない要素を探していたことになります。アクションタイムアウトもないので、そのクリックはテストの持ち時間が尽きるまで待つしかありませんでした。30秒の失敗が5つで2分30秒。実行全体は3.8分でした。</p>
<p>私が最初に足したのはリトライでした。30分もしないうちに設定には <code>retries: process.env.CI ? 2 : 1</code> が入り、そこに付けたコメントには今も &ldquo;for flaky browser crashes&rdquo;（ときどき起きるブラウザのクラッシュ対策）と書いてあります。次の実行では、同じ5つのうち4つがリトライも含めてまた失敗し、実行時間は3.8分から6.4分に延びました。これらのdescribeブロックはシリアルモードで動きます。1つが失敗するとグループの残りはスキップされ、リトライはグループを最初からやり直します。そのせいで、壊れたクリックはどれも30秒を2回ずつ待ちました。毎回同じ理由で失敗するテストは、リトライでは救えません。代償を2回払うだけです。</p>
<p>修正は前方一致でした。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="nx">page</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid^=&#34;template-item-&#34;]&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">filter</span><span class="p">({</span> <span class="nx">hasText</span><span class="o">:</span> <span class="s2">&#34;E2E Edit Template&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">item</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;edit-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span></span></span></code></pre></div></div>
<p>もっと広い教訓は、テストIDも一種の契約だということです。私には、このID 1つに頼るテストスイートが2つあり、どちらもお互いの存在を知りませんでした。Playwrightのベストプラクティスのガイドが、私が使ったようなCSSセレクターよりも <code>getByRole()</code> のようなユーザーから見える属性に基づくロケーターを勧めているのも、ユーザーが気づきもしないDOMの変更でテストが壊れないようにするためです。</p>

<h2 class="relative group">止まったステップを見つける
    <div id="止まったステップを見つける" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%ad%a2%e3%81%be%e3%81%a3%e3%81%9f%e3%82%b9%e3%83%86%e3%83%83%e3%83%97%e3%82%92%e8%a6%8b%e3%81%a4%e3%81%91%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>まずコールログを読みます。これは時間切れの瞬間にまだ動いていたステップのもので、<code>waiting for locator(...)</code> の後に何も続いていなければ、ロケーターが最後まで何にも一致しなかったということです。</p>
<p>それで足りなければトレースを開きます。私のアプリのスイート2つ、見積もりアプリのものと、私が作っているマーケットプレイス兼コミュニティアプリのものは、どちらも <code>trace: &quot;on-first-retry&quot;</code> を使っています。最初のリトライのときだけ記録する設定なので、<code>retries</code> が1以上でないと記録されません。リトライを使わないなら <code>&quot;retain-on-failure&quot;</code>、1回だけ試すなら <code>--trace on</code> です。あとはトレースを直接開くか、HTMLレポートから開きます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright show-trace test-results/&lt;test-folder&gt;/trace.zip
</span></span><span class="line"><span class="cl">npx playwright show-report</span></span></code></pre></div></div>
<p>終わらなかったアクションは、一覧の最後のほうでエラーが付いている項目です。それを選んでスナップショットを見てください。Playwrightが待っていた時点のページそのもので、期待していた要素を自分で探せます。私のケースなら、テストが探していないIDを付けた行が、ちゃんとそこにあったはずです。</p>
<p>最近のバージョンは、失敗時のスクリーンショットの隣に <code>error-context.md</code> も書き出します。失敗した瞬間のページをテキストのスナップショットにしたもので、要素が見つからないケースなら、これだけで十分なこともよくあります。</p>
<p>落とし穴が1つあります。<code>--debug</code> はInspectorを開くと同時に、<code>--max-failures=1</code>、<code>--headed</code>、<code>--workers=1</code> と一緒に <code>--timeout=0</code> も設定します。<code>--debug</code> で通ったテストは、30秒については何も教えてくれていません。</p>

<h2 class="relative group">すべてのアクションに上限を
    <div id="すべてのアクションに上限を" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%81%99%e3%81%b9%e3%81%a6%e3%81%ae%e3%82%a2%e3%82%af%e3%82%b7%e3%83%a7%e3%83%b3%e3%81%ab%e4%b8%8a%e9%99%90%e3%82%92" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>私の設定ファイルには、アクションタイムアウトを設定しているものが1つもありません。だから見つからない要素1つのために、30秒を5回も使ったわけです。<code>use</code> に2行足せば変わります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@playwright/test&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">30_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="o">:</span> <span class="p">{</span> <span class="nx">timeout</span>: <span class="kt">5_000</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">use</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">actionTimeout</span>: <span class="kt">10_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">navigationTimeout</span>: <span class="kt">15_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">trace</span><span class="o">:</span> <span class="s2">&#34;retain-on-failure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>すると同じ壊れたロケーターが、テストにまだ時間が残っているうちに、こう失敗します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">    TimeoutError: locator.click: Timeout 10000ms exceeded.
</span></span><span class="line"><span class="cl">    Call log:
</span></span><span class="line"><span class="cl">      - waiting for locator(&#39;[data-testid=&#34;template-item&#34;]&#39;).filter({ hasText: &#39;E2E Edit Template&#39; }).locator(&#39;[data-testid=&#34;edit-template&#34;]&#39;)</span></span></code></pre></div></div>
<p>どのステップで、どのアクションが、どれだけ待ったのかが全部わかります。2つの値はテストタイムアウトより十分小さくしておくのがいいと思います。そうすれば、止まったステップ1つが持ち時間を使い切り、曖昧なメッセージの陰に隠れることはありません。2月の問題は30秒ではありませんでした。あのクリックにかかっていた時計がそれ1つだけだったので、それが鳴ったというだけです。</p>

<h2 class="relative group">タイムアウトを延ばすのは長いテストだけ、止まったテストではなく
    <div id="タイムアウトを延ばすのは長いテストだけ止まったテストではなく" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%bf%e3%82%a4%e3%83%a0%e3%82%a2%e3%82%a6%e3%83%88%e3%82%92%e5%bb%b6%e3%81%b0%e3%81%99%e3%81%ae%e3%81%af%e9%95%b7%e3%81%84%e3%83%86%e3%82%b9%e3%83%88%e3%81%a0%e3%81%91%e6%ad%a2%e3%81%be%e3%81%a3%e3%81%9f%e3%83%86%e3%82%b9%e3%83%88%e3%81%a7%e3%81%af%e3%81%aa%e3%81%8f" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>タイムアウトを延ばすのが正解なのは、テストが実際に時間のかかる仕事をしているときです。あるステップが起こらないことを待っているときには不正解です。数字を大きくしても、失敗が遅れて届くだけですから。どちらなのかは、たいていトレースを見ればはっきりします。アクションが1つずつ終わっていて単に数が多いなら長いテスト、1つのアクションが最後まで <code>waiting for</code> のまま止まっているなら止まったテストです。</p>
<p>長いテストなら、スイート全体ではなく、長さがある場所に時間を足します。マーケットプレイスアプリのスイートには、スクリーンショットを撮りながらチャット画面を一通り回るスモークテストが1つあり、このテストには3分を与えています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;chat: open, send a message, go back&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="mi">180</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p><code>describe</code> の中で <code>test.setTimeout()</code> を呼ぶと、グループ内のすべてのテストに効きます。見積もりアプリのスイートの作業コスト（job-cost）のテストが1分をもらっているのは、この方法です。<code>test.slow()</code> は今のタイムアウトが何であれ3倍にし、条件を渡すこともできます。<code>beforeEach</code> の中で <code>testInfo.setTimeout()</code> を使えば、そのフックを通るすべてのテストが延びます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s2">&#34;slow in WebKit&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span><span class="p">,</span> <span class="nx">browserName</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="p">.</span><span class="nx">slow</span><span class="p">(</span><span class="nx">browserName</span> <span class="o">===</span> <span class="s2">&#34;webkit&#34;</span><span class="p">,</span> <span class="s2">&#34;This page is slow in WebKit&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">test</span><span class="p">.</span><span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">},</span> <span class="nx">testInfo</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">testInfo</span><span class="p">.</span><span class="nx">setTimeout</span><span class="p">(</span><span class="nx">testInfo</span><span class="p">.</span><span class="nx">timeout</span> <span class="o">+</span> <span class="mi">30</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div>
<p>持ち時間を毎回静かに削っていくものが、もう1つあります。固定のスリープです。見積もりアプリのスイートには、ほぼすべてのspecファイルにまたがって、<code>page.waitForTimeout()</code> の呼び出しが100個以上あります。上の編集テストはクリックにたどり着く前に3秒眠っていますが、どれだけ眠っても助からなかったでしょう。PlaywrightのAPIドキュメントも、ここははっきりしています。&ldquo;Never wait for timeout in production. Tests that wait for time are inherently flaky.&quot;（本番では決して時間で待たないこと。時間を待つテストは本質的に不安定です。）Webファーストアサーションは、もともと必要なだけ待ち、それ以上は待ちません。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="c1">// before
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForTimeout</span><span class="p">(</span><span class="mi">1500</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// after
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">locator</span><span class="p">(</span><span class="s1">&#39;[data-testid=&#34;save-template&#34;]&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">getByText</span><span class="p">(</span><span class="s2">&#34;E2E Edited Template&#34;</span><span class="p">)).</span><span class="nx">toBeVisible</span><span class="p">();</span></span></span></code></pre></div></div>
<p>アサーションのタイムアウトにも同じ注意が要ります。2つのアプリのスイートはどちらも <code>expect.timeout</code> を15秒に上げていて、どちらも本番ビルドではなくアプリの開発サーバーを相手に動いています。余裕はできますが、遅いアサーションが2つ続けば、その2つで30秒を使い切ることもあります。そうなるとテストタイムアウトが先に切れて、また曖昧なメッセージに逆戻りです。</p>

<h2 class="relative group">テストの時計ではないとき
    <div id="テストの時計ではないとき" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%86%e3%82%b9%e3%83%88%e3%81%ae%e6%99%82%e8%a8%88%e3%81%a7%e3%81%af%e3%81%aa%e3%81%84%e3%81%a8%e3%81%8d" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>メッセージに <code>config.webServer</code> が出てくるなら、まだテストは1つも動いていません。Playwrightは <code>webServer</code> のコマンドを起動し、その <code>url</code> が応答するまで待ってからテストを始めます。この待ち時間には別のタイムアウトがあり、デフォルトは60秒です。2つのアプリのスイートは、どちらも開発サーバーに2分を与えています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="cl"><span class="nx">webServer</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">command</span><span class="o">:</span> <span class="s2">&#34;npm run web -- --port 8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">url</span><span class="o">:</span> <span class="s2">&#34;http://localhost:8081&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">reuseExistingServer</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">timeout</span>: <span class="kt">120_000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span></span></span></code></pre></div></div>
<p>テストがローカルのサーバーではなくデプロイ済みの環境に向いているなら、最初のリクエストにどれだけかかるかを確かめてください。使っているホスティング先でようやく測ってみたら、ゼロスケールから起動するマシンが何かを返すまでに25〜33秒かかっていました。最初のクリックの前に、テストの持ち時間のほとんど、あるいは全部です。その話は「<a href="https://jared.lynskey.co.nz/ja/posts/2026/2026-09-09-scale-to-zero-504s/" >ゼロスケールと、再起動のたびに起きる30秒の障害</a>」に書きました。</p>

<h2 class="relative group">「No tests found」と出たら
    <div id="no-tests-foundと出たら" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#no-tests-found%e3%81%a8%e5%87%ba%e3%81%9f%e3%82%89" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p><code>No tests found</code> は似て見えますが、タイムアウトではありません。Playwrightが探したものの、実行するものが何もなかったという意味です。デフォルトでは <code>testDir</code> の下で <code>**/*.@(spec|test).?(c|m)[jt]s?(x)</code> に一致するファイルを集め、<code>testDir</code> のデフォルトは設定ファイルがあるフォルダーです。私の設定は設定ファイルの隣の <code>testDir: &quot;./tests&quot;</code> を指しているので、ほかの場所に保存したspecは見えません。コマンドラインの引数はファイルパスに対してマッチさせる正規表現で、だからエラーには &ldquo;Make sure that arguments are regular expressions matching test files.&rdquo; という一文が付きます。何も実行せずに、Playwrightが何を集めたかだけを見るにはこうします。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npx playwright <span class="nb">test</span> --list</span></span></code></pre></div></div>
<p>その一覧にspecがないなら、そもそもタイムアウトになりようがありません。Playwrightをこれから導入するところなら、スクリーンショット比較から始めようという私の考えは「<a href="https://jared.lynskey.co.nz/ja/posts/2024/2024-06-09-playwright-tests/" >小規模チームこそビジュアルテストを</a>」に書いています。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-playwright-timeout-30000ms/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>XcodeでiOSアプリをデバッグする方法：ブレークポイント、LLDB、Instruments、シミュレータと実機</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-02-26-debugging-ios-mobile-apps/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-02-26-debugging-ios-mobile-apps/</guid>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>iOS</category>
      <category>Xcode</category>
      <category>Debugging</category>
      <category>Swift</category>
      <category>iOS Simulator</category>
      <category>Mobile Development</category>
      <description>XcodeでiOSアプリをデバッグする方法をまとめました。条件付き・シンボリックブレークポイント、覚えておきたいLLDBコマンド、ビューデバッガとメモリグラフ、Instruments、サニタイザー、クラッシュログ、そしてシミュレータでは見つからず実機で初めて分かるバグまで。</description>
      <content:encoded><![CDATA[<p>XcodeでiOSアプリをデバッグするなら、まず例外ブレークポイントを追加して、クラッシュしたときに例外がスローされた行で止まるようにします。次にバグを再現し、LLDBコンソールの <code>po</code> で状態を確かめます。あとは症状に合わせてツールを選ぶだけです。レイアウトの崩れにはビューデバッガ、リークや遅さにはメモリグラフとInstruments、たまにしか起きないクラッシュにはAddress SanitizerとThread Sanitizer。そして最後は必ず実機で確認します。シミュレータでは表に出てこない種類のバグがあるからです。</p>
<p>デバッグには本当に時間を取られます。誰でもそうだと思います。最近は自分のアプリのバグ追いでXcodeとシミュレータにこもる日が多く、そのたびに思うのは、Xcodeにはほとんど誰も触らない強力なデバッグ機能が山ほどある、ということです。この記事では、長年かけて落ち着いた自分のデバッグ手順のうち、実際に時間の節約になっている部分をまとめます。</p>

<h2 class="relative group">ブレークポイントは思っているより強力
    <div id="ブレークポイントは思っているより強力" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88%e3%81%af%e6%80%9d%e3%81%a3%e3%81%a6%e3%81%84%e3%82%8b%e3%82%88%e3%82%8a%e5%bc%b7%e5%8a%9b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ほとんどの人は、ガターをクリックしてブレークポイントを置いて終わりです。でも右クリックしてみると、デバッグのやり方が変わるくらいのオプションが出てきます。</p>

<h3 class="relative group">条件付きブレークポイント
    <div id="条件付きブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9d%a1%e4%bb%b6%e4%bb%98%e3%81%8d%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>1000件を処理するループで、347番目だけがおかしいとします。346回もブレークポイントで止まるのは時間の無駄です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">0.</span><span class="p">.&lt;</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">processUser</span><span class="p">(</span><span class="n">users</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>  <span class="c1">// ここにブレークポイントを設定、条件: i == 347</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>ブレークポイントを右クリックして条件に <code>i == 347</code> を入れれば、Xcodeは条件が真のときだけ止まります。<code>userId == &quot;abc123&quot;</code> や <code>error != nil</code> のような条件でも同じです。</p>

<h3 class="relative group">シンボリックブレークポイント
    <div id="シンボリックブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b7%e3%83%b3%e3%83%9c%e3%83%aa%e3%83%83%e3%82%af%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>メソッドがどこから呼ばれているのか分からないときに使います。ブレークポイントナビゲータ（Cmd+8）で「+」ボタンをクリックし、「Symbolic Breakpoint」を選びます。</p>
<p>どのビューコントローラがいつロードされるか全部見たければ、シンボルを <code>viewDidLoad</code> にします。Xcodeがすべての呼び出しで止まります。うるさすぎるなら <code>MyViewController.viewDidLoad</code> のように絞り込みましょう。</p>
<p>私が常設しているのはこの3つです。</p>
<ul>
<li><code>UIViewAlertForUnsatisfiableConstraints</code>：Auto Layoutの競合をその場で捕まえる</li>
<li><code>objc_exception_throw</code>：クラッシュする前にObjective-Cの例外で止まる</li>
<li><code>malloc_error_break</code>：メモリ割り当ての問題を見つける</li>
</ul>

<h3 class="relative group">例外ブレークポイント
    <div id="例外ブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%be%8b%e5%a4%96%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>新しいプロジェクトで最初にやるべき設定だと思います。ブレークポイントナビゲータで「+」→「Exception Breakpoint」を追加しておくと、アプリが落ちた後ではなく、何かがスローされた瞬間にXcodeが止まってくれます。</p>
<p>コンソールに謎のクラッシュとしてしか出てこないnilアンラップの問題を、これで何度も捕まえてきました。</p>

<h3 class="relative group">ブレークポイントアクション
    <div id="ブレークポイントアクション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88%e3%82%a2%e3%82%af%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ブレークポイントは実行を止めなくてもいいんです。ブレークポイントを編集して「Add Action」をクリックすると、こんなことができます。</p>
<ul>
<li>止めずに変数を出力する：<code>&quot;User count: @(users.count)@&quot;</code></li>
<li>LLDBコマンドを自動実行する</li>
<li>シェルスクリプトを実行する</li>
<li>音を鳴らす（めったに通らないコードパスの確認に使っています）</li>
</ul>
<p>「Automatically continue after evaluating actions」にチェックを入れれば、ブレークポイントはフローを止めないロガーになります。</p>

<h2 class="relative group">LLDB：実際に使うコンソールコマンド
    <div id="lldb実際に使うコンソールコマンド" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#lldb%e5%ae%9f%e9%9a%9b%e3%81%ab%e4%bd%bf%e3%81%86%e3%82%b3%e3%83%b3%e3%82%bd%e3%83%bc%e3%83%ab%e3%82%b3%e3%83%9e%e3%83%b3%e3%83%89" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ブレークポイントで止まったとき、下のコンソールはクラッシュログを眺めるだけの場所ではありません。あれはLLDBで、本当に便利です。</p>

<h3 class="relative group">基本
    <div id="基本" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9f%ba%e6%9c%ac" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po user
▿ User
  - id: &#34;123&#34;
  - name: &#34;John Doe&#34;
  - email: &#34;john@example.com&#34;</code></pre></div>
<p><code>po</code> はオブジェクトを読みやすい形で出力します。私のLLDB利用の9割はこれです。</p>
<p>もう少し詳しく見たいときは <code>p</code> を使います。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) p user.name
(String) $R0 = &#34;John Doe&#34;</code></pre></div>
<p>ローカル変数を一度に全部見るなら、こうです。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) frame variable</code></pre></div>

<h3 class="relative group">実行中に値を変える
    <div id="実行中に値を変える" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e8%a1%8c%e4%b8%ad%e3%81%ab%e5%80%a4%e3%82%92%e5%a4%89%e3%81%88%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>LLDBの本領はここです。再コンパイルせずに変数を書き換えられます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) expr user.name = &#34;Jane Doe&#34;
(lldb) expr index = 0</code></pre></div>
<p>バグを見つけて、リビルドせずに修正を試したい。そんなときは変数を変えてそのまま続行すればいいわけです。エッジケースを絞り込むとき、いつもこれで値を差し替えています。</p>
<p>メソッドの呼び出しもできます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)</code></pre></div>

<h3 class="relative group">ナビゲーション
    <div id="ナビゲーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%8a%e3%83%93%e3%82%b2%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) bt              # コールスタックを表示
(lldb) frame select 3  # 別のスタックフレームにジャンプ
(lldb) continue        # 実行を続ける（または単に &#39;c&#39;）
(lldb) n               # ステップオーバー（次の行）
(lldb) s               # 関数にステップイン</code></pre></div>

<h3 class="relative group">ウォッチポイント
    <div id="ウォッチポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%a6%e3%82%a9%e3%83%83%e3%83%81%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>変数がいつ変わるのか知りたければウォッチポイントです。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) watchpoint set variable user.isLoggedIn</code></pre></div>
<p>コードのどこから変更されても、その瞬間に実行が止まります。想定外の状態変更を追うときには手放せません。</p>

<h2 class="relative group">Console.app：隠れたデバッグツール
    <div id="consoleapp隠れたデバッグツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp%e9%9a%a0%e3%82%8c%e3%81%9f%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Xcodeのコンソールで十分な場面は多いですが、システムログもクラッシュレポートも全部見たい、となったらConsole.appの出番です。</p>
<p>OSLogを使っていれば（使うべきです）、Console.appでログを検索したりフィルタしたりできます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;networking&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Starting API request&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">&#34;Request URL: </span><span class="si">\(</span><span class="n">url</span><span class="p">.</span><span class="n">absoluteString</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">error</span><span class="p">(</span><span class="s">&#34;Failed to decode: </span><span class="si">\(</span><span class="n">error</span><span class="p">.</span><span class="n">localizedDescription</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Console.app側の手順はこうです。</p>
<ol>
<li>シミュレータか接続中のデバイスを選ぶ</li>
<li>サブシステムで絞る：<code>subsystem:com.example.app</code></li>
<li>レベルで絞る：<code>subsystem:com.example.app AND level:error</code></li>
</ol>
<p>よく使うフィルタ述語は保存できます。私はネットワークリクエスト用、データベース操作用、エラーと警告だけを出す用の3つを持っています。</p>

<h3 class="relative group">print()ではなくOSLogを使う理由
    <div id="printではなくoslogを使う理由" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#print%e3%81%a7%e3%81%af%e3%81%aa%e3%81%8foslog%e3%82%92%e4%bd%bf%e3%81%86%e7%90%86%e7%94%b1" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>OSLogは、<code>print()</code> をあちこちに撒くよりずっとましです。構造化されているのでレベルやカテゴリで絞り込めますし、ログ自体が最適化されていてアプリを遅くしません。本番では機密データを自動でマスクしてくれて、アプリが落ちた後もログは残ります。</p>
<p>とっさの確認には <code>print()</code> で構いません。後で見返す可能性があるものはOSLogに残しましょう。</p>

<h3 class="relative group">Console.appの高度なフィルタリング
    <div id="consoleappの高度なフィルタリング" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp%e3%81%ae%e9%ab%98%e5%ba%a6%e3%81%aa%e3%83%95%e3%82%a3%e3%83%ab%e3%82%bf%e3%83%aa%e3%83%b3%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Console.appはかなり凝った述語クエリに対応しています。私がよく使うのはこのあたりです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># アプリ内のすべてのエラーと障害
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND (level:error OR level:fault)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 失敗したネットワークリクエスト
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND category:networking AND eventMessage CONTAINS &#34;failed&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 過去5分間のすべて
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND timestamp &gt;= now(-5m)</span></span></code></pre></div></div>
<p>ログはエクスポートしてバグレポートに添付することもできます。Xcodeのコンソールからコピペするよりずっと楽です。</p>

<h2 class="relative group">ビューデバッグ
    <div id="ビューデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%93%e3%83%a5%e3%83%bc%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>UIが壊れているのに原因が分からないとき、いちばん早いのはビューデバッガです。</p>
<p>アプリを実行して壊れた画面まで進み、Xcodeのデバッグバーで「Debug View Hierarchy」をクリックします（またはCmd+Shift+D）。ビュー階層全体が3Dで分解表示されます。</p>
<p>回してみると、いろいろ見えてきます。</p>
<ul>
<li>他のビューの裏でレンダリングされているビュー</li>
<li>画面のはるか外にいるビュー</li>
<li>サイズがゼロのビュー</li>
<li>選択したビューの制約チェーン全体</li>
</ul>
<p>私はこれで、タップイベントを吸い込んでいた透明なボタンや、0x0でレンダリングされていたラベル、いつの間にか幅10,000ピクセルになっていた画像を見つけたことがあります。</p>

<h3 class="relative group">Auto Layoutのデバッグ
    <div id="auto-layoutのデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#auto-layout%e3%81%ae%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ビュー階層に紫の警告アイコンが出ていたら制約の競合です。クリックすると、どの制約同士がぶつかっているのかXcodeが正確に教えてくれます。</p>
<p>制約には識別子を付けておきましょう。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">heightConstraint</span><span class="p">.</span><span class="n">identifier</span> <span class="p">=</span> <span class="s">&#34;ProfileImageHeight&#34;</span></span></span></code></pre></div></div>
<p>制約が壊れたとき、エラーにメモリアドレスではなく <code>&quot;ProfileImageHeight&quot;</code> が出るようになります。ログの読みやすさが段違いです。</p>
<p>LLDBから制約ツリーを出力することもできます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()</code></pre></div>

<h2 class="relative group">時間を節約してくれるシミュレータ機能
    <div id="時間を節約してくれるシミュレータ機能" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%99%82%e9%96%93%e3%82%92%e7%af%80%e7%b4%84%e3%81%97%e3%81%a6%e3%81%8f%e3%82%8c%e3%82%8b%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e6%a9%9f%e8%83%bd" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">スローアニメーション
    <div id="スローアニメーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%ad%e3%83%bc%e3%82%a2%e3%83%8b%e3%83%a1%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Slow Animations で、すべてが1/10の速度になります。トランジション中に何が起きているのか、アニメーションがなぜ変に見えるのかを確かめるのにちょうどいいです。</p>

<h3 class="relative group">メモリ警告のシミュレート
    <div id="メモリ警告のシミュレート" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e8%ad%a6%e5%91%8a%e3%81%ae%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Simulate Memory Warning。メモリ不足のときのアプリの挙動をテストできます。これで画像キャッシュのバグをいくつも見つけました。メモリ警告が来るまでは普通に動いているのに、来た途端に画像が全部消えるやつです。</p>

<h3 class="relative group">Network Link Conditioner
    <div id="network-link-conditioner" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#network-link-conditioner" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Xcode → Open Developer Tools → Network Link Conditioner</p>
<p>3GやLTE、高パケットロス、完全オフラインをシミュレートできます。WiFiでは平気なAPIリクエストがモバイル回線でタイムアウトする、という経験があるなら、原因はこれで再現できます。</p>
<p>私は遅延500ms・パケットロス10%の「Bad Network」プロファイルを常備しています。速いオフィスWiFiでは絶対に出ない、タイムアウトのバグやローディング表示の問題が引っかかります。</p>

<h3 class="relative group">ステータスバーのオーバーライド
    <div id="ステータスバーのオーバーライド" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%bc%e3%82%bf%e3%82%b9%e3%83%90%e3%83%bc%e3%81%ae%e3%82%aa%e3%83%bc%e3%83%90%e3%83%bc%e3%83%a9%e3%82%a4%e3%83%89" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>シミュレータのステータスバーを右クリックすると、時刻、バッテリー残量、電波強度、キャリアを上書きできます。スクリーンショットを揃えたいときや、バッテリー残量ごとのUI確認に便利です。</p>

<h3 class="relative group">位置情報のシミュレーション
    <div id="位置情報のシミュレーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bd%8d%e7%bd%ae%e6%83%85%e5%a0%b1%e3%81%ae%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Location で、机から動かずにいろいろな場所を再現できます。任意の座標、街歩き、高速道路の走行まで一通り揃っています。位置情報を使う機能のテストや、特定の地域でしか起きない不具合の調査で重宝します。</p>

<h2 class="relative group">メモリデバッグ
    <div id="メモリデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Debug Memory Graph
    <div id="debug-memory-graph" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#debug-memory-graph" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug Memory Graphボタン（Cmd+Shift+M）を押すと、メモリ上のすべてのオブジェクトとその参照関係、そして肝心の「どれがリークしているか」をXcodeが見せてくれます。</p>
<p>紫のビックリマークがリークです。クリックすると保持サイクルが見えます。</p>
<p>いちばんよく見かけるリークはこれです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">ViewController</span><span class="p">:</span> <span class="n">UIViewController</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">onComplete</span><span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">setupHandler</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ❌ selfを強く参照</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>weak selfで直します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">?.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ✓ 保持サイクルなし</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>デリゲートもweakにしておきます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">weak</span> <span class="kd">var</span> <span class="nv">delegate</span><span class="p">:</span> <span class="n">ManagerDelegate</span><span class="p">?</span>  <span class="c1">// 単なる &#39;var&#39; ではなく</span></span></span></code></pre></div></div>

<h3 class="relative group">Instruments
    <div id="instruments" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#instruments" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>本腰を入れてメモリを調べるならInstrumentsです（Product → Profile、またはCmd+I）。</p>
<p>Allocationsを使うと、オブジェクトの割り当てすべて、時間経過に伴うメモリの増え方、どのクラスがメモリを食っているか、割り当てが起きた場所のスタックトレースまで見えます。</p>
<p>私が探すのはだいたい、時間に比例して増え続けるメモリ（ほぼリークです）、想定外に大きい割り当て（画像1000枚をうっかり一気に読んでいないか？）、解放されているはずなのに残っているオブジェクト、この3つです。</p>
<p>操作の前後で世代をマークして（小さな旗のボタン）、残ってはいけないものが残っていないか確認しましょう。</p>

<h2 class="relative group">パフォーマンスプロファイリング
    <div id="パフォーマンスプロファイリング" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%91%e3%83%95%e3%82%a9%e3%83%bc%e3%83%9e%e3%83%b3%e3%82%b9%e3%83%97%e3%83%ad%e3%83%95%e3%82%a1%e3%82%a4%e3%83%aa%e3%83%b3%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Time Profiler
    <div id="time-profiler" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#time-profiler" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Product → Profile → Time Profiler。アプリで重い操作をしながら記録して、コールツリーを読みます。</p>
<p>「Self Weight」でソートするとボトルネックが見つかります。ある関数のself weightが40%なら、時間はそこに消えています。</p>
<p>以前、JSONのパース関数が毎秒1000回呼ばれているのを見つけたことがあります。バックグラウンドキューに移したら、UIのカクつきが止まりました。</p>

<h3 class="relative group">Main Thread Checker
    <div id="main-thread-checker" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#main-thread-checker" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>バックグラウンドスレッドでのUI更新は、Xcodeが自動で捕まえてくれます。こんな表示が出たら、</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Main Thread Checker: UI API called on a background thread: -[UILabel setText:]</span></span></code></pre></div></div>
<p>こういうことをやってしまっています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ❌ クラッシュ！</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>直すとこうなります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ✓</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h2 class="relative group">ランタイム診断
    <div id="ランタイム診断" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a9%e3%83%b3%e3%82%bf%e3%82%a4%e3%83%a0%e8%a8%ba%e6%96%ad" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Edit Scheme → Run → Diagnostics。ここに並んでいるチェックボックスは、手作業では絶対に見つからないバグを捕まえてくれます。</p>

<h3 class="relative group">Address Sanitizer
    <div id="address-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#address-sanitizer" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>メモリ破壊を見つけます。use-after-free、バッファオーバーフロー、メモリリーク。アプリは2〜3倍遅くなりますが、他の方法ではほぼ追跡不可能なバグを捕まえてくれます。たまにしか起きないクラッシュを追うときにオンにします。</p>

<h3 class="relative group">Thread Sanitizer
    <div id="thread-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thread-sanitizer" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>データ競合とスレッド絡みの問題を捕まえます。有効にしてアプリを普通に触っているだけで、競合状態があればThread Sanitizerが見つけてくれます。</p>
<p>Address SanitizerとThread Sanitizerは同時に使えないので、妙なクラッシュを追うときは交互に切り替えています。</p>

<h3 class="relative group">Zombie Objects
    <div id="zombie-objects" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zombie-objects" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>解放済みオブジェクトへのメッセージを捕まえます。有効にしておくと、解放されたメモリに触った瞬間をXcodeが正確に教えてくれます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000</span></span></code></pre></div></div>
<p>つけっぱなしは禁物です。オブジェクトが解放されなくなるので、メモリは増える一方になります。ただ、特定のuse-after-freeバグを突き止めるには最高です。</p>

<h2 class="relative group">よくあるデバッグシナリオ
    <div id="よくあるデバッグシナリオ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%88%e3%81%8f%e3%81%82%e3%82%8b%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%82%b7%e3%83%8a%e3%83%aa%e3%82%aa" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">起動時にクラッシュする
    <div id="起動時にクラッシュする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b5%b7%e5%8b%95%e6%99%82%e3%81%ab%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>まず例外ブレークポイントを追加します。たいていはこれで捕まります。</p>
<p>だめなら、Info.plistのキー漏れ、初期化コードでの強制アンラップ、依存性注入の失敗あたりを疑います。</p>

<h3 class="relative group">UIが更新されない
    <div id="uiが更新されない" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ui%e3%81%8c%e6%9b%b4%e6%96%b0%e3%81%95%e3%82%8c%e3%81%aa%e3%81%84" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>毎回、原因はこのどれかです。</p>
<ol>
<li>更新がメインスレッドで行われていない</li>
<li>アウトレットがつながっていない</li>
<li>ビューがそもそも表示されていない（LLDBで <code>po view.window</code> を確認。nilなら階層に入っていません）</li>
</ol>
<p>テーブルビューやコレクションビューなら、<code>reloadData()</code> の呼び忘れです。</p>

<h3 class="relative group">メモリ警告でアプリが落ちる
    <div id="メモリ警告でアプリが落ちる" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e8%ad%a6%e5%91%8a%e3%81%a7%e3%82%a2%e3%83%97%e3%83%aa%e3%81%8c%e8%90%bd%e3%81%a1%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Instruments Allocationsで何がメモリを掴んでいるか確認します。たいていは、キャッシュから解放されない画像、解放されないビューコントローラ（保持サイクル）、際限なく育つ配列や辞書のどれかです。</p>
<p>シミュレータでメモリ警告をシミュレートして、クリーンアップ処理をテストしておきましょう。</p>

<h3 class="relative group">原因不明のクラッシュ
    <div id="原因不明のクラッシュ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%9f%e5%9b%a0%e4%b8%8d%e6%98%8e%e3%81%ae%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>サニタイザーを全部オンにします。間欠的ならたぶんスレッドの問題なのでThread Sanitizerで、システムフレームワーク内で落ちているならたぶんメモリ破壊なのでAddress Sanitizerで走らせます。</p>

<h3 class="relative group">再現できないクラッシュ
    <div id="再現できないクラッシュ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%86%8d%e7%8f%be%e3%81%a7%e3%81%8d%e3%81%aa%e3%81%84%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>クラッシュが他人のスマホで起きた場合でも、レポートはたいていどこかに残っています。TestFlightやApp Storeのビルドなら、XcodeのOrganizer（Window → Organizer → Crashes）に、解析データの共有をオンにしているユーザーのクラッシュレポートが集まってきます。ビルドと一緒にdSYMもアップロードしてあれば、シンボル化された状態で見られます。テスターの端末なら、レポートは端末そのものの「設定 → プライバシーとセキュリティ → 解析および改善 → 解析データ」に、アプリ名で始まる <code>.ips</code> ファイルとして残っているので、AirDropで送ってもらいましょう。まずはクラッシュしたスレッドのスタックトレースを読み、そこから上のシナリオを順に当たっていきます。</p>

<h3 class="relative group">シミュレータでOTA更新をデバッグする
    <div id="シミュレータでota更新をデバッグする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e3%81%a7ota%e6%9b%b4%e6%96%b0%e3%82%92%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>私のアプリはOTA（Over-the-Air）アップデートを配信しているのですが、このフロー全体のデバッグにはシミュレータがいちばん手軽です。</p>
<p>まずネットワークトラフィックを見ます。アプリのサブシステムで絞ったConsole.appに、マニフェストのリクエストとレスポンスがリアルタイムで流れてくるので、どのヘッダーを送っていてサーバーが何を返しているかが正確に分かります。</p>
<p>次に、わざとネットワークを悪くします。Network Link Conditionerで、遅い回線やパケットロスのときに更新フローがどう振る舞うかを確かめます。アプリは固まらないか。ローディング表示は出るか。ちゃんとフォールバックするか。</p>
<p>更新チェックの周辺にはログを仕込みます。チェック開始時、更新があったとき、ダウンロード開始時、新しいバンドルのロード時、それぞれで記録しています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;updates&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Checking for updates...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">update</span> <span class="p">=</span> <span class="k">try</span> <span class="n">await</span> <span class="n">Updates</span><span class="p">.</span><span class="n">checkForUpdateAsync</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Update available: </span><span class="si">\(</span><span class="n">update</span><span class="p">.</span><span class="n">isAvailable</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>最後に、マニフェストのエンドポイント単体もテストしておくと安心です。アプリが送るのと同じヘッダーでcurlできます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -H <span class="s2">&#34;expo-protocol-version: 1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-platform: ios&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-runtime-version: 1.0.0&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     https://updates.example.com/manifest</span></span></code></pre></div></div>
<p>クライアント側を掘り始める前に、サーバーが正しいデータを返しているかをここで確認できます。</p>

<h2 class="relative group">サードパーティツール
    <div id="サードパーティツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b5%e3%83%bc%e3%83%89%e3%83%91%e3%83%bc%e3%83%86%e3%82%a3%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Charles Proxy / Proxyman
    <div id="charles-proxy--proxyman" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#charles-proxy--proxyman" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ネットワークトラフィックの全表示、リクエストとレスポンスの改変、エラー条件のテスト。APIデバッグには欠かせません。</p>
<p>ProxymanはCharlesよりUIがよく、macOSに馴染む感じがします。去年乗り換えて、戻る気はありません。</p>

<h3 class="relative group">Reveal
    <div id="reveal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#reveal" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Xcodeのビューデバッガに似ていますが、もっと強力です。Xcodeにつながずに実機で動くので、テスターの端末でレイアウト問題を追うのに向いています。</p>

<h2 class="relative group">シミュレータと実機の違い
    <div id="シミュレータと実機の違い" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e3%81%a8%e5%ae%9f%e6%a9%9f%e3%81%ae%e9%81%95%e3%81%84" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>デバッグの大半はシミュレータでやっています。ケーブルも署名もいらず、インストールを待つ必要もないので速いからです。とはいえ、シミュレータの正体はMacのプロセッサの上で動くiOSのビルドで、メモリもネットワーク接続もMacのものをそのまま使っています。スマホの上でしか起きないバグは、そこでは出てきません。</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>シミュレータ</th>
					<th>実機のiPhone</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>速度</td>
					<td>MacのCPUで動くので、たいていスマホより速い</td>
					<td>本当の数字が出る。パフォーマンスの計測はこちらで</td>
			</tr>
			<tr>
					<td>メモリ</td>
					<td>実質的に上限がないので、メモリ警告はシミュレートする</td>
					<td>使いすぎたアプリはシステムに終了させられる</td>
			</tr>
			<tr>
					<td>ネットワーク</td>
					<td>Macの回線そのまま</td>
					<td>モバイル回線、弱い電波、WiFiとモバイルデータの切り替え</td>
			</tr>
			<tr>
					<td>カメラ、Bluetooth、NFC</td>
					<td>使えない</td>
					<td>使える</td>
			</tr>
			<tr>
					<td>プッシュ通知</td>
					<td><code>xcrun simctl push</code> で疑似的に送れる</td>
					<td>ユーザーが受け取るのと同じ経路で届く</td>
			</tr>
			<tr>
					<td>試行の速さ</td>
					<td>数秒</td>
					<td>遅い（署名、インストール、ケーブルかWiFi）</td>
			</tr>
	</tbody>
</table>
<p>なので、ロジックの組み立てとデバッグはシミュレータで済ませ、リリース前にはサポート対象の中でいちばん古いiPhoneでアプリを動かしています。パフォーマンス、メモリの逼迫、ハードウェアに触れる部分は、実機で動かすまでは本当の意味でテストしたことになりません。</p>

<h3 class="relative group">実機で動かす
    <div id="実機で動かす" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e6%a9%9f%e3%81%a7%e5%8b%95%e3%81%8b%e3%81%99" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>iPhoneをつないで実行先に選び、Signing &amp; Capabilities でチームを設定します（自分のスマホで動かすだけなら無料のApple IDで足ります）。iOS 16以降は、スマホ側でも「設定 → プライバシーとセキュリティ」でデベロッパモードをオンにする必要があり、初回は再起動がかかります。オンにすると、スマホ自体の「設定 → デベロッパ」メニューにNetwork Link Conditionerが現れ、Macと同じ悪条件のネットワークプロファイルが使えるようになります。</p>

<h3 class="relative group">ワイヤレスデバッグ
    <div id="ワイヤレスデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%af%e3%82%a4%e3%83%a4%e3%83%ac%e3%82%b9%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>一度だけUSBでつなぎ、Window → Devices and Simulators で「Connect via network」にチェックを入れて、ケーブルを抜きます。同じWiFiにいる限り、デバイスはXcodeから見え続けます。</p>

<h3 class="relative group">Device Console
    <div id="device-console" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#device-console" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Window → Devices and Simulators → Open Console。システムログ、アプリログ、クラッシュレポートと、何でも見えます。Xcode側のコンソールよりずっと詳細です。</p>
<p>テスターから「アプリが落ちた」と言われたら、必ずデバイスをつないでコンソールログを取らせてもらいます。すべてを説明してくれるシステムレベルのエラーが残っていることが多いんです。</p>

<h2 class="relative group">実際のデバッグ手順
    <div id="実際のデバッグ手順" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e9%9a%9b%e3%81%ae%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e6%89%8b%e9%a0%86" aria-label="アンカー">#</a>
    </span>
    
</h2>
<ol>
<li>例外ブレークポイントがなければ追加する</li>
<li>バグを再現する</li>
<li>怪しい場所の近くにブレークポイントを置く</li>
<li>LLDBの <code>po</code> で値を確認する</li>
<li>変数を書き換えて、再コンパイルせずに修正を試す</li>
<li>UI絡みならビュー階層を見る</li>
<li>パフォーマンス絡みならInstrumentsでプロファイルする</li>
<li>メモリかスレッド絡みならサニタイザーをオンにする</li>
</ol>
<p>コツは症状から逆算することです。クラッシュ？例外ブレークポイント。遅い？Time Profiler。メモリ？Instruments。UIが変？ビューデバッガ。</p>

<h2 class="relative group">もっと早く知りたかったこと
    <div id="もっと早く知りたかったこと" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%82%e3%81%a3%e3%81%a8%e6%97%a9%e3%81%8f%e7%9f%a5%e3%82%8a%e3%81%9f%e3%81%8b%e3%81%a3%e3%81%9f%e3%81%93%e3%81%a8" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>アサーションを使いましょう。ロジックの誤りがバグになる前に捕まります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="bp">assert</span><span class="p">(</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#34;Users array should never be empty here&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>デバッグを始める前にコミットしておくこと。仮説を試すためにあちこち書き換え始めると、必ず戻したくなります。</p>
<p>終わったらprint文は消すこと。少なくとも <code>#if DEBUG</code> で囲んでおきましょう。古いデバッグログは、次のバグを見えにくくします。</p>
<p>LLDBはちゃんと覚えること。アプリを20回リビルドするより速いです。</p>
<p>そして、シミュレータは本物の電話ではありません。実機でしか出ないバグは、ほぼ例外なくスレッドかメモリの問題です。あるいはパフォーマンスの問題。シミュレータはiPhoneのチップではなくMacのCPUで動いていますから。</p>

<h2 class="relative group">常に開いているツール
    <div id="常に開いているツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e3%81%ab%e9%96%8b%e3%81%84%e3%81%a6%e3%81%84%e3%82%8b%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>
<ul>
<li>Xcode（当然）</li>
<li>アプリのサブシステムで絞ったConsole.app</li>
<li>ネットワークデバッグ用のProxyman</li>
<li>事態が深刻になってきたらInstruments</li>
</ul>
<hr>
<p>どのツールも万能ではありません。ビューデバッガはメモリリークを見つけてくれませんし、InstrumentsはAuto Layoutの競合を解いてくれません。結局のところ、腕の見せどころは症状に合ったツールを選ぶことで、それは回数をこなすうちに身につきます。保持サイクルを何度も踏めば、スタックトレースを見た瞬間に匂いで分かるようになります。デバッグが楽しくなることはたぶんないですが、少なくとも苦行ではなくなります。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-02-26-debugging-ios-mobile-apps/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
