<?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>IOS · 仁才徳</title>
    <link>https://jared.lynskey.co.nz/ja/tags/ios/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/tags/ios/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Sentryをセルフホストしない選択：SDKはそのまま、React Nativeのクラッシュレポートは自前サーバーで</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-10-09-sentry-sdk-own-server/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-10-09-sentry-sdk-own-server/</guid>
      <pubDate>Fri, 09 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>React Native</category>
      <category>Expo</category>
      <category>Sentry</category>
      <category>Django</category>
      <category>クラッシュレポート</category>
      <category>セルフホスト</category>
      <category>iOS</category>
      <category>Android</category>
      <category>プライバシー</category>
      <description>Sentryをセルフホストせず、sentry.ioの費用もかけずに、React Nativeアプリのクラッシュレポートを受ける方法です。オープンソースのSDKはそのままに、DSNを自前のDjango APIに向け、グルーピング、アラート、データの除去まで自分で行います。</description>
      <content:encoded><![CDATA[<p>React Nativeアプリのクラッシュレポートを自分で受けたいなら、Sentryのサーバーをセルフホストする必要も、sentry.ioに費用を払う必要もありません。SentryのSDKはオープンソースで、DSNはどのホストにも向けられます。私は自前のDjango APIのエンドポイントに向け、そこでレポートを受け取り、グルーピングし、アラートを出し、保存しています。費用は、もともと動いていたAPIの分だけです。まだ置き換えられていないのはシンボリケーションで、スタックトレースからはソースの行まではたどれません。</p>
<p>今週まで、うちのアプリがクラッシュしたことを知る手段は、誰かが教えてくれることだけでした。ほとんどの人は教えてくれません。アプリを開き直し、また落ちたら、もう開かなくなるだけです。API側は把握できていました（ERRORログはすべてチームのチャットチャンネルに流れます）。でも、スマートフォン上でのクラッシュは、私に見える場所には何の痕跡も残していませんでした。</p>
<p>実は、アプリにはすでに報告の仕組みがひとつありました。アプリが誰かの前にエラーメッセージを表示すると、そのメッセージをAPIの小さなエンドポイントに送り、サーバーがERRORでログに残すので、これもチャットチャンネルに届きます。きっかけは、削除が <em>成功していた</em> のに行がリストに残り、もう一度タップすると「No Item matches the given query」と表示された件でした。サーバーはごく普通の404を2回記録しただけで、何も言いませんでした。この仕組みでカバーできるのは、アプリが捕まえて表示したエラーです。クラッシュは事情が違います。報告を送るはずのJavaScriptそのものが、たった今死んだのですから。</p>
<p>素直な解決策はSentryです。SentryのSDKはとても気に入っていて、React Nativeのクラッシュレポートが「解決済みの問題」になっているのはこのSDKのおかげです。ただ、ユーザーデータを預ける業者がまたひとつ増え、プライバシーポリシーの行がまたひとつ増え、うちの規模なら多くても1日数件のレポートのためにまた請求書が増えるのは避けたいところでした。そこでSDKはそのままに、サービスの方を置き換えました。アプリのDSNは自前のAPIを指していて、そこのDjangoアプリの小さな一部分が、送られてきたものを受け取り、グルーピングし、アラートを出し、保存します。</p>
<p>この記事では、その仕組みと、本物のネイティブクラッシュが実際に届くことをどう確かめたかを紹介します。</p>

<h2 class="relative group">DSNのからくり
    <div id="dsnのからくり" 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="#dsn%e3%81%ae%e3%81%8b%e3%82%89%e3%81%8f%e3%82%8a" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Sentry の DSN は <code>https://&lt;public-key&gt;@&lt;host&gt;/&lt;project-id&gt;</code> という形をしています。SDKはこれを <code>https://&lt;host&gt;/api/&lt;project-id&gt;/envelope/</code> へのPOSTに変換し、キーは <code>X-Sentry-Auth</code> ヘッダーに入れます。sentry.io固有の要素はどこにもありません。なので、アプリに埋め込むDSNはこうなります。</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">https://&lt;CRASH_REPORT_KEY&gt;@api.example.com/1</span></span></code></pre></div></div>
<p>そしてDjango側には、SDKが呼び出すパスに一致するルートを用意します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">path</span><span class="p">(</span><span class="s2">&#34;api/&lt;int:project_id&gt;/envelope/&#34;</span><span class="p">,</span> <span class="n">crash_envelope</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&#34;crash_envelope&#34;</span><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-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">crash_envelope</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">project_id</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">expected</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">CRASH_REPORT_KEY</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">expected</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Response</span><span class="p">({</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;Crash reporting is not configured&#34;</span><span class="p">},</span> <span class="n">status</span><span class="o">=</span><span class="mi">503</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">_key</span><span class="p">(</span><span class="n">request</span><span class="p">)</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span> <span class="n">expected</span><span class="o">.</span><span class="n">encode</span><span class="p">()):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Response</span><span class="p">({</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;Unknown key&#34;</span><span class="p">},</span> <span class="n">status</span><span class="o">=</span><span class="mi">401</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span></span></span></code></pre></div></div>
<p><code>_key</code> は <code>X-Sentry-Auth</code> ヘッダーから <code>sentry_key=</code> を読み取り、なければクエリ文字列を見ます。SDKによってはそちらを使うためです。</p>

<h2 class="relative group">SDKが送ってくるもの：エンベロープ
    <div id="sdkが送ってくるものエンベロープ" 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="#sdk%e3%81%8c%e9%80%81%e3%81%a3%e3%81%a6%e3%81%8f%e3%82%8b%e3%82%82%e3%81%ae%e3%82%a8%e3%83%b3%e3%83%99%e3%83%ad%e3%83%bc%e3%83%97" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Sentryの通信フォーマットは<a href="https://develop.sentry.dev/sdk/data-model/envelopes/"  target="_blank" rel="noreferrer">エンベロープ</a>です。JSONのヘッダー行が1行あり、その後に任意の数のアイテムが続きます。各アイテムはJSONのヘッダー行とペイロードの組です。アイテムヘッダーに <code>length</code> があればペイロードはちょうどそのバイト数、なければ次の改行までです。</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">{&#34;event_id&#34;:&#34;9ec79c33...&#34;,&#34;dsn&#34;:&#34;https://...@api.example.com/1&#34;}
</span></span><span class="line"><span class="cl">{&#34;type&#34;:&#34;event&#34;,&#34;length&#34;:4123}
</span></span><span class="line"><span class="cl">{ ...the event JSON, 4123 bytes... }
</span></span><span class="line"><span class="cl">{&#34;type&#34;:&#34;client_report&#34;}
</span></span><span class="line"><span class="cl">{&#34;timestamp&#34;:1791400000.5,&#34;discarded_events&#34;:[]}</span></span></code></pre></div></div>
<p>引っかかった点が二つあります。</p>
<ul>
<li><strong>ネイティブSDKは本文をgzipしますが、JavaScriptのSDKはしません。</strong> <code>@sentry/react-native</code> が捕捉したJSエラーはプレーンテキストで届き、sentry-cocoaやsentry-androidからのネイティブクラッシュは <code>Content-Encoding: gzip</code> 付きで届きます。デコーダーはgzip、zlibでラップされたdeflate、生のdeflateに対応し、展開は1MBで打ち切ります。クラッシュイベントは数KBなので、それより大きいものはクラッシュイベントではありません。</li>
<li><strong>長さ指定のペイロードは改行を含むことがあります。</strong> <code>\n</code> で分割すると、ネイティブSDKが送るイベントがすべて壊れます。<code>length</code> があるときは必ずそれに従う必要があります。</li>
</ul>
<p>パーサーは短いものです。残すのは <code>event</code> アイテムだけで、セッション、トランザクション、添付ファイル、クライアントレポートは受け取ったうえで捨てます。</p>

<h2 class="relative group">SDKが理解できる形で応答する
    <div id="sdkが理解できる形で応答する" 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="#sdk%e3%81%8c%e7%90%86%e8%a7%a3%e3%81%a7%e3%81%8d%e3%82%8b%e5%bd%a2%e3%81%a7%e5%bf%9c%e7%ad%94%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>これは思っていたより大事でした。SDKはディスク上にキューを持ち、届いたか確信が持てないものはすべて再送します。なので、サーバーの応答次第で、SDKが送り続けるかどうかが決まります。</p>
<ul>
<li><strong>200</strong>：保存できたとき、 <em>または意図的に捨てたとき</em> 。セッションアイテムに4xxを返すと、SDKはそれを保持したまま永遠に再送します。</li>
<li><strong>429と <code>Retry-After</code></strong> ：スロットリングしたとき。エンドポイントにはIPごとの制限（1時間に120件）があります。クラッシュループに入ったスマートフォンは起動のたびに1件送ってくるからです。SDKは429を受けると、期待どおりに間隔を空けてくれます。</li>
<li><strong>400</strong>：壊れたエンベロープ。ログはエラーではなく警告にしています。壊れたレポートが教えてくれるのは「どこかのクライアントがおかしい」ということで、次のまともなレポートが何がクラッシュしたかを教えてくれます。</li>
</ul>
<p>SDKは最初の送信が届いたか確信が持てないと同じイベントを再送するので、<code>event_id</code> にはユニーク制約を付けてあり、重複は何も言わずに無視されます。</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%bd%e3%83%bc%e3%82%b9%e3%83%9e%e3%83%83%e3%83%97%e3%81%aa%e3%81%97%e3%81%a7%e3%81%ae%e3%82%b0%e3%83%ab%e3%83%bc%e3%83%94%e3%83%b3%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ここは、自作の受信側がSentryなら代わりにやってくれる判断を自分でしなければならない部分です。Sentryはスタックトレースでクラッシュをグルーピングします。でも、リリース版のHermesバンドルは難読化されているので、フレーム名は1文字になり、行や列の位置はビルドのたびに変わります。フレームでグルーピングすると、同じバグがリリースのたびに新しいissueになってしまいます。</p>
<p>ソースマップがない状態（最後に詳しく書きます）では、JavaScriptのエラーで一番安定しているのは型とメッセージです。そこでフィンガープリントは、例外の型とメッセージを組み合わせ、発生ごとに変わる部分をマスクしたものにしました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">_MASKS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;0x[0-9a-fA-F]+&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;addr&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\b[0-9a-fA-F]</span><span class="si">{8}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{12}</span><span class="s2">\b&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;uuid&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\b[0-9a-fA-F]{16,}\b&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;hex&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\d+&#34;</span><span class="p">),</span> <span class="s2">&#34;&lt;n&gt;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span></span></span></code></pre></div></div>
<p><code>Cannot read property 'title' of undefined (reading index 3)</code> と <code>(reading index 17)</code> は同じissueになります。SDKが <code>{{ default }}</code> を含まない独自の <code>fingerprint</code> を送ってきたらそちらを優先するので、デフォルトのグルーピングが合わないときはクライアント側から強制できます。例外を持たないイベントはマスクしたログメッセージで、どちらもないイベントはアプリ内の一番上のフレームでグルーピングします。ネイティブのシグナルクラッシュではメッセージが <code>Signal 6, Code 0</code> のようになるので、シグナル単位でまとまります。粗いですが、シンボリケーションができるまでは正直なやり方です。</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%81%e3%83%a3%e3%83%b3%e3%83%8d%e3%83%ab%e3%82%92%e5%9f%8b%e3%82%81%e5%b0%bd%e3%81%8f%e3%81%95%e3%81%aa%e3%81%84%e3%82%a2%e3%83%a9%e3%83%bc%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>フィンガープリントごとにissueが1つでき、各レポートはその下にイベントとして保存されます。issueがERRORでログを出す（チャットのアラートが待ち受けているのはこれです）のは、次の場合だけです。</p>
<ul>
<li><strong>新しい</strong> issue</li>
<li><strong>解決済み</strong> のissueが <strong>再発</strong> したとき（issueは再オープンされます）</li>
<li><strong>10回目、100回目、1,000回目、10,000回目</strong> の発生</li>
</ul>
<p>通常の繰り返しはカウンターを増やすだけです。 <em>無視</em> にしたissueはカウントは続けますが、アラートは一切出しません。その上にチャットのハンドラー自体のレート制限があるので、ダメなリリースでクラッシュループが起きても、届くのは壁のような大量のメッセージではなく、数件で済みます。</p>
<p>保存量も同じように上限を設けています。各issueは <strong>直近50件</strong> のイベントだけを残し、新しいものが来るたびに古いものを削除します。これで最初のグラフが壊れました。50件のイベントでは、先週どれくらいの頻度で起きていたかは分からないからです。そこで各issueに90日分の日ごとの件数も持たせ、カウンターを増やすのと同じトランザクションの中で更新するようにしました。グラフはこの件数から描き、イベントは読むためだけのものです。</p>
<p>自分で作るなら、小さな競合状態が二つ気にかける価値があります。まったく新しいクラッシュが同時に何台ものスマートフォンで起きると、二つのレポートが両方ともissueを作ろうとすることがあるので、フィンガープリントをユニークにして、負けた方が既存の行を読み直すようにします。また、90日の範囲は一番新しいレポートからではなく、サーバーの今日から数えます。時計がずれたスマートフォンは、来年の日付も含めてどんな日付のレポートでも送ってくる可能性があるからです。</p>
<p>ペイロードは128KBが上限です。超えたイベントはまずパンくずリストとデバッグ用のメタデータを削り、それでも大きければ例外、メッセージ、リリース、プラットフォームだけを残して、削ったことが分かる印を付けます。</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%87%e3%83%bc%e3%82%bf%e3%81%ae%e9%99%a4%e5%8e%bb" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>SDKは <code>sendDefaultPii: false</code> で動いているので、原理的にはIPアドレスもCookieもリクエストボディも送られません。それでもサーバーはこれに頼りません。保存する前に <code>user</code> からは <code>id</code> と <code>username</code> だけを残し、<code>request</code> は丸ごと捨てます。将来のSDKのバージョンや設定でこれらが送られ始めたときのためです。</p>
<p>実機でテストしたところ、除去すべきものがさらに二つ見つかりました。</p>
<ul>
<li><strong>パンくずリストはクエリ文字列も含めたURL全体を記録します。</strong> アプリの検索リクエストは検索語をクエリ文字列に載せて送るので、検索直後にクラッシュすると、その人が何を検索したかまで保存されていたことになります。今はサーバーがパンくずリストのURLをすべて <code>?</code> で切っています。</li>
<li><strong>コンソールのパンくずリストには <code>console.log</code> に渡された生の引数が入っています。</strong> アプリがたまたまログに出した任意のオブジェクトが入りうるということです。メッセージ本文は役に立つので残し、引数は捨てます。</li>
</ul>
<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%af%e3%83%a9%e3%82%a4%e3%82%a2%e3%83%b3%e3%83%88%e5%81%b4" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>アプリでは、すべてが小さなモジュール1つを通ります。OTAアップデートのSDKをラップしているのと同じ理由で、OTAバンドルはネイティブモジュールが存在する前にビルドされたバイナリの上でも動くからです。<code>RNSentry</code> を持たないバイナリでトップレベルに <code>import * as Sentry from &quot;@sentry/react-native&quot;</code> と書くとimportの時点で例外が投げられ、画面が真っ白になります。なので、SDKは遅延して <code>require</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="kr">import</span> <span class="p">{</span> <span class="nx">NativeModules</span><span class="p">,</span> <span class="nx">Platform</span><span class="p">,</span> <span class="nx">TurboModuleRegistry</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;react-native&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">Constants</span> <span class="kr">from</span> <span class="s2">&#34;expo-constants&#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">const</span> <span class="nx">DSN</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">EXPO_PUBLIC_CRASH_DSN</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">nativeModulePresent</span><span class="p">()</span><span class="o">:</span> <span class="kr">boolean</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">Boolean</span><span class="p">(</span><span class="nx">TurboModuleRegistry</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s2">&#34;RNSentry&#34;</span><span class="p">)</span> <span class="o">??</span> <span class="nx">NativeModules</span><span class="p">.</span><span class="nx">RNSentry</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">false</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><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">initCrashReporting</span><span class="p">()</span><span class="o">:</span> <span class="kr">boolean</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">DSN</span> <span class="o">||</span> <span class="nx">Platform</span><span class="p">.</span><span class="nx">OS</span> <span class="o">===</span> <span class="s2">&#34;web&#34;</span> <span class="o">||</span> <span class="o">!</span><span class="nx">nativeModulePresent</span><span class="p">())</span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">Sentry</span> <span class="o">=</span> <span class="kr">require</span><span class="p">(</span><span class="s2">&#34;@sentry/react-native&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Sentry</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dsn</span>: <span class="kt">DSN</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">release</span>: <span class="kt">Constants.expoConfig?.version</span> <span class="o">??</span> <span class="s2">&#34;unknown&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">sendDefaultPii</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">tracesSampleRate</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">enableAutoSessionTracking</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...turn off the other tracing options the same way
</span></span></span><span class="line"><span class="cl">    <span class="nx">maxBreadcrumbs</span>: <span class="kt">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">beforeSend</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* tag the OTA bundle */</span> <span class="k">return</span> <span class="nx">event</span><span class="p">;</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="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>扱うのはエラーだけです。サーバーはトレース、セッション、プロファイル、リプレイを保存しないので、どれも有効にしていません。DSNがなければ（開発時やE2Eのビルド）SDKは起動すらしません。ビルドプロファイルがDSNと環境（<code>staging</code> か <code>production</code>）を設定するので、どちらのビルドも同じエンドポイントに報告しつつ、区別できます。ログイン中のユーザーはアカウントIDで設定し、名前やメールアドレスは一切使いません。</p>
<p>エラーバウンダリには明示的な呼び出しが必要でした。Expo Routerのルートの <code>ErrorBoundary</code> はレンダリングエラーを捕まえて独自の画面を表示するので、そのエラーはSDKがフックしているグローバルハンドラーまで届きません。解決策は小さなラッパーで、<code>useEffect</code> の中で <code>captureException</code> を呼んでから、これまでどおりルーターのバウンダリを描画します。ルートのバウンダリにもルートごとのバウンダリにも同じものを使います。レンダリング中ではなくエフェクトの中で呼んでいるのは、再レンダリングのたびにではなく1回だけ報告するためです。ルートコンポーネントは <code>Sentry.wrap</code> でもラップしていて、それが例外を投げたら素のコンポーネントに戻ります。</p>
<p>Web向けには、全関数を無効にした <code>.web.ts</code> 版のモジュールを置いています。Metroがプラットフォーム別のファイルを選ぶので、SDKがWebのバンドルに入ることはありません。Webアプリには動かすべきネイティブモジュールがないからです。</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%8d%e3%82%a4%e3%83%86%e3%82%a3%e3%83%96%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5%e3%81%8c%e5%b1%8a%e3%81%8f%e3%81%93%e3%81%a8%e3%82%92%e8%a8%bc%e6%98%8e%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ユニットテストは手書きのエンベロープをPOSTするもので、証明できるのは、SDKが送ってくると <em>私が思っている</em> ものをパーサーが処理できる、ということだけです。リリースから2日後、本番のクラッシュレポートはゼロでした。アップデートしたのはまだ3人だけだったので、ゼロでもおかしくはありません。でも「誰もクラッシュしていない」と「クラッシュが届いていない」は、サーバーから見るとまったく同じに見えます。本物を見たいと思いました。</p>
<p>そこで、DSNをポート8000のローカルDjangoと使い捨てのキーに向けた、Releaseビルドのシミュレーター用バイナリを作りました。</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"><span class="nv">EXPO_PUBLIC_CRASH_DSN</span><span class="o">=</span>http://localcrashtestkey0000@localhost:8000/1 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">EXPO_PUBLIC_CRASH_ENVIRONMENT</span><span class="o">=</span>local-verify <span class="se">\
</span></span></span><span class="line"><span class="cl">xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release <span class="se">\
</span></span></span><span class="line"><span class="cl">  -destination <span class="s1">&#39;platform=iOS Simulator,id=&lt;udid&gt;&#39;</span> -derivedDataPath /tmp/iosbuild</span></span></code></pre></div></div>
<p>（Debugではなく Release です。Debugビルドはdev launcherをリンクするので挙動が違いますし、ユーザーが使っているのはReleaseです。）それからインストールして起動し、数秒待ってから、本物の障害と同じやり方で強制終了させました。</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">xcrun simctl install booted /tmp/iosbuild/Build/Products/Release-iphonesimulator/MyApp.app
</span></span><span class="line"><span class="cl">xcrun simctl launch booted com.example.myapp      <span class="c1"># prints the pid</span>
</span></span><span class="line"><span class="cl"><span class="nb">kill</span> -ABRT &lt;pid&gt;</span></span></code></pre></div></div>
<p>シミュレーターのアプリは普通のmacOSのプロセスなので、シグナルは実機とまったく同じようにSentryCrashのハンドラーに届きます。ハンドラーはレポートをディスクに書き込むだけで、プロセスが死にかけている間は何も送りません。 <strong>次の起動時</strong> にSDKがそれを見つけて送信します。再起動すると、1秒後にシミュレーターのネットワークログに <code>:8000</code> への接続が出ました。</p>
<p>ところがDjangoのログには何も出ず、少しの間混乱しました。開発サーバーは、私が設定していたログレベルでは2xx以外の応答しかログに出さないのです。データベースにはちゃんと入っていました。</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">SIGABRT: Signal 6, Code 0 | cocoa 1.4.0 iOS 27.1 iPhone19,4 handled=False
</span></span><span class="line"><span class="cl">received 09:19:53, crashed 09:19:38
</span></span><span class="line"><span class="cl">contexts: app, device, os, ota_updates, trace · 24 breadcrumbs</span></span></code></pre></div></div>
<p>クラッシュから保存まで15秒で、そのほとんどは私が再起動していた時間です。そして本物のペイロードを見たことで、テストでは分からなかったことがいくつも分かりました。</p>
<ul>
<li><strong>ユーザーIDがアカウントIDではありませんでした。</strong> テスト用ビルドは実際にはログインしていないので、sentry-cocoaはインストールごとの独自IDを使っていました。知っておく価値があり、プライバシーの説明にも書くべきことです。レポートには、ログインしていればアカウントのID、していなければランダムなインストールIDが入ります。</li>
<li><strong>タイムゾーンがありません。</strong> cultureコンテキストがあるものと思い込んでいました。deviceコンテキストには <code>locale</code>、メモリ、画面サイズ、省電力モード、カーネルのバージョンはありますが、タイムゾーンはありません。</li>
<li><strong>ネイティブクラッシュは <code>beforeSend</code> を通りません。</strong> JSの <code>beforeSend</code> で実行中のOTAバンドルを示す <code>ota</code> タグを付けていますが、ネイティブクラッシュは次の起動時にネイティブSDKが組み立てるので、JSのフックを一度も通りません。ネイティブクラッシュにタグを付けるには、バンドルが分かった時点で <code>setTag</code> を使ってスコープに設定し、ネイティブ層に同期させる必要があります。</li>
<li><strong>パンくずリストからアプリの動きが見えました。</strong> コンソールの警告、ビューコントローラーのライフサイクル、HTTPの呼び出し。その中には分析SDK自身による収集用エンドポイントへのリクエストも含まれていて、アプリが他にどこと通信しているかを思い出させてくれます。そしてURLにはクエリ文字列が付いていて、これで先ほどの検索語の問題に気づきました。</li>
</ul>

<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%ac%e3%83%9d%e3%83%bc%e3%83%88%e3%82%92%e8%aa%ad%e3%82%80" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>レポートはDjangoの管理画面で読みます。Sentryをゆるく真似たissueページで、上部に件数とResolve / Ignore / Reopenボタン、日ごとの棒グラフ、リリース・OS・端末・OTAバンドル別の内訳があり、その下でイベントを1件ずつ、例外、フレーム（アプリのフレームは強調し、ライブラリのフレームは折りたたみます）、パンくずリスト、端末のコンテキストと一緒に表示します。新しい依存関係は要りませんでした。</p>
<p>作っている途中で一つ修正が出ました。ネイティブクラッシュでは「原因（culprit）」フレームが端末上のバイナリのパスになっていて、そこにインストールごとのUUIDが含まれるため、同じクラッシュが端末ごとに違って見えていました。今は <code>0x1000a1b2c (MyApp)</code> のように、アドレスとバイナリ名だけを表示します。</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%97%e3%83%a9%e3%82%a4%e3%83%90%e3%82%b7%e3%83%bc%e3%83%9d%e3%83%aa%e3%82%b7%e3%83%bc%e3%82%92%e3%81%be%e3%82%8b%e3%81%94%e3%81%a8" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>クラッシュレポートは、ほぼ個人情報です。アカウントID、スマートフォンの機種、そしてアプリが死ぬ直前の1分間にその人が何をしていたか。なので、プライバシーポリシーに1段落必要でした。きちんと書くにはレポートに何が入っているかを正確に書く必要があったので、さっき見たばかりのペイロードから列挙しました。エラー、アプリのバージョンとOTAアップデート、機種、OSと言語、空きメモリと画面サイズ、直前の数十ステップ（開いた画面、 <code>?</code> 以降を取り除いたWebアドレス、アプリ自身のログ）、そしてアカウントIDかランダムなインストールID。ポリシーには、レポートが自社のサーバーに届き、Sentryやその他のクラッシュレポート会社には送られないことも書いています。保存期間は表に専用の行を設けました。問題ごとに直近50件のレポートと、90日分の日ごとの件数です。</p>
<p>一つの段落をコードと突き合わせ始めると、残りを確認しないわけにはいきませんでした。既存のポリシーは耐えられませんでした。アカウント削除ページには、アカウントを閉じるとプッシュ通知のトークンとパスキーは即座に削除され、サーバーログは90日以内に消える、と書かれていました。どれも事実ではありませんでした。アカウントを閉じるとサインイン方法の紐付けが外れてプロフィールは匿名化されますが、プッシュ通知のトークンとパスキーは誰かが手で消すまで残りますし、ログのアーカイブにはまだ有効期限がありません。そこでポリシー全体を、コードが実際に何をしているかの棚卸しから書き直し、それらの行は今は「まだ自動では削除されません。メールでご連絡ください」となっています。安心感は減りますが、正確です。</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="#%e8%b6%b3%e3%82%8a%e3%81%aa%e3%81%84%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p><strong>シンボリケーション。</strong> これが本当の穴で、sentry.ioがやっていて私がまだ置き換えていない唯一のものです。スタックはJavaScriptでは難読化され、ネイティブコードではシンボル化されていません。</p>
<ul>
<li><strong>JavaScript：</strong> フレームはHermesバンドルの中を指しています。直すには、ストア向けビルドとOTAバンドルのすべてについて合成済みのHermesソースマップをリリースとバンドルをキーに保存しておき、ページを描画するときにJSのフレームに適用する必要があります。うちのクラッシュの大半はJavaScriptになるはずなので、次に取りかかるのはこれです。</li>
<li><strong>iOSのネイティブ：</strong> 命令アドレスにはビルドのdSYMが必要です。</li>
<li><strong>Android：</strong> 難読化（minification）を有効にするとJavaのフレームはR8で難読化され（<code>mapping.txt</code> が必要）、C++のフレームにはNDKのシンボルが必要です。</li>
</ul>
<p>先ほど見つけた、もう少し小さな穴もあります。OTAタグは <code>beforeSend</code> でしか付けていないので、ネイティブクラッシュにはまだ付きません。実行中のバンドルが確定した時点でスコープに設定するのは数行ですが、まだ書いていません。</p>
<p>今のところ、issueから分かるのは、 <em>何が</em> クラッシュしたか、どのリリース、OTAバンドル、端末でか、そしてその直前に何をしていたかです。ソースの <em>どこで</em> かは分かりません。週に数回リリースする小さなアプリなら、これまでは型とメッセージとパンくずリストで十分に原因を見つけられています。</p>
<p>受信側、管理画面、テストを合わせてもコードは多くなく、すでに動いていたAPIの上に追加のコストはかかっていません。難しい部分はSDKがやってくれます。スマートフォン上でクラッシュを捕まえ、次の起動時にレポートを送り出すことです。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-09-sentry-sdk-own-server/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>
