3/9

2026

WidgetKit .description()の文字列補間でFatal Error — watchOS Widget開発の罠

#WidgetKit#watchOS#Swift#デバッグ#Apple WatchWidgetKit .description()の文字列補間でFatal Error — watchOS Widget開発の罠

WidgetKit .description()の文字列補間でFatal Error

PlayTimeのWatch Smart Stack1対応で、WidgetKit2.description()に文字列補間を使ったらWidget Extension3が即死した。Xcodeのコンソールにはエラーが一切表示されず、原因特定に時間がかかった。

この記事では、何を間違えていたのか、なぜ気づけなかったのか、どうやって検知したのかを記録する。

何を間違えていたか

.description()で文字列補間を使った

WidgetKitのIntentConfiguration4(またはStaticConfiguration)には.configurationDisplayName().description()という2つのメソッドがある。Widget候補の表示名と説明文を設定するものだ。

// ❌ これがFatal Errorの原因
var body: some WidgetConfiguration {
    IntentConfiguration(
        kind: "PlayTimeWidget",
        intent: PlayTimeIntent.self,
        provider: Provider()
    ) { entry in
        PlayTimeWidgetView(entry: entry)
    }
    .configurationDisplayName("PlayTime")  // ← これは動く
    .description("Quick access to \(widgetDisplayName)")  // ← 💥 Fatal Error
}

.description()\(widgetDisplayName)のような文字列補間5を渡すと、以下のエラーでWidget Extensionプロセスが即座にクラッシュする:

Fatal error: Formatted text for description is not supported

configurationDisplayName()との非対称性

厄介なのは、.configurationDisplayName()では文字列補間が動くことだ。

.configurationDisplayName("\(appName) Widget")  // ✅ 動く
.description("\(appName) Widget")                // ❌ Fatal Error

同じLocalizedStringKey6を受け取るAPIなのに、一方は動いて他方はクラッシュする。この非対称性が罠になっている。.configurationDisplayName()で補間が動いたから.description()でも使えるだろうと思うのは自然な発想だが、実際にはFatal Errorになる。

⚠️ .description()の内部実装はLocalizedStringKeyのフォーマット済みテキストを処理できない。これはWidgetKit側の制限で、Appleのドキュメントには明記されていない

ハマりポイント — エラーが見えない

Widget Extensionのクラッシュは表示されない

この問題を特に厄介にしているのは、Widget Extension3のクラッシュがXcodeのコンソール7に表示されないことだ。

Widget Extensionはメインアプリとは別プロセスで動作する。Xcodeのデバッガはメインアプリのプロセスにアタッチしているため、Extension側のFatal Errorはコンソールに出力されない。

Xcodeコンソール:
  ✅ メインアプリのログ → 全て表示される
  ❌ Widget Extensionのログ → 表示されない(別プロセス)

結果として、「ビルドは通る」「メインアプリは正常」「エラーゼロに見える」のに、WidgetがSmart Stackの候補に出てこないという不可解な状況になる。

⚡ Widget Extensionがクラッシュしても、iOSもwatchOSもユーザーにエラーを表示しない。ただ静かにExtensionが消える。Smart Stackの候補リストから消えるだけで、クラッシュしたことすら分からない。

Fatal Errorの検知方法

simctl log showでExtensionプロセスのログを見る

Widget Extensionのログを確認するには、xcrun simctl8でシミュレータのログを直接参照する必要がある。

# Watch SimulatorのUDIDを取得
WATCH_UDID=$(xcrun simctl list devices | grep "Apple Watch" | grep "Booted" | \
  head -1 | grep -oE "[0-9A-F-]{36}")

# Widget Extensionプロセスのログを表示(直近5分間)
xcrun simctl spawn "$WATCH_UDID" log show \
  --predicate 'process == "PlayTimeWidgetExtension"' \
  --last 5m

このログの中に以下のようなFatal Errorメッセージが含まれている:

Fatal error: Formatted text for description is not supported

chronodログで起動段階のクラッシュを確認

watchOSのchronod9プロセスはWidget Extensionのライフサイクルを管理している。Extensionが正常に起動してcomplication descriptor10を登録できたかどうかは、chronodのログで確認できる。

# chronodのログを確認
xcrun simctl spawn "$WATCH_UDID" log show \
  --predicate 'process == "chronod"' \
  --last 5m | grep -i "complication\|descriptor\|extension"

正常な場合:

Successfully wrote complication descriptor for PlayTimeWidgetExtension

クラッシュしている場合: 上記のメッセージが出力されない。Extensionが起動段階でクラッシュしているため、descriptorの登録まで到達していない。

companion app未インストール時の挙動

Watch SimulatorにiPhoneのcompanion app11がインストールされていない場合、以下のログが出力される:

Ignoring restricted or unknown extension: com.example.app.PlayTimeWidgetExtension

このメッセージは.description()のクラッシュとは別の問題だが、Widget Extensionが認識されない原因として混同しやすい。companion appが正しくインストールされているか確認してから、.description()の問題を疑うこと。

シミュレータキャッシュの罠

修正してもWidgetが候補に出ない

.description()を修正してリビルドしても、一度クラッシュしたWidget Extensionのキャッシュがシミュレータに残っていて、候補リストに出てこないことがある。

# ❌ リビルドだけでは不十分なことがある
# Xcodeでビルド → インストール → Smart Stackに候補が出ない

simctl eraseでリセット

シミュレータのキャッシュをクリアするには、xcrun simctl eraseでリセットが必要。

# Watch Simulatorをリセット
xcrun simctl erase "$WATCH_UDID"

⚠️ simctl eraseはシミュレータの全データを消去する。他のアプリのデータも失われるので注意。ただし、Widget Extensionのキャッシュ問題を確実に解消するにはこれが最も確実な方法。

リセット後、以下の手順で再確認:

  1. Xcodeでメインアプリ + Widget Extensionをビルド・インストール
  2. Watch Simulatorでアプリを起動(companion appとして認識させる)
  3. Smart Stackの編集画面でWidget候補を確認

修正

静的文字列リテラルを使う

修正は単純。.description()には文字列補間を使わず、静的な文字列リテラル12を渡す。

// ✅ 修正後: 静的文字列リテラルを使用
var body: some WidgetConfiguration {
    IntentConfiguration(
        kind: "PlayTimeWidget",
        intent: PlayTimeIntent.self,
        provider: Provider()
    ) { entry in
        PlayTimeWidgetView(entry: entry)
    }
    .configurationDisplayName("PlayTime")
    .description("Quick access to PlayTime")  // ← 静的文字列リテラル
}

動的な値を表示名や説明文に含めたい場合は、ローカライズファイル(.strings.stringsdict)を使う方法がある。ただし、Widget Extensionの.description()はシステムUIに表示される説明文なので、ほとんどの場合は静的文字列で十分だ。

💡 .configurationDisplayName().description()の違いを覚えておくだけで、このFatal Errorは完全に回避できる。.description()には文字列補間を入れない。これだけ。

まとめ

問題原因対処
.description()でFatal Error文字列補間が内部でサポートされていない静的文字列リテラルを使う
エラーがXcodeに表示されないWidget Extensionは別プロセスsimctl log showでExtensionログを確認
修正後もWidgetが候補に出ないシミュレータキャッシュsimctl eraseでリセット
companion appが認識されない未インストール or 不一致companion appを先にインストール

Widget Extensionのデバッグは、メインアプリとは異なるアプローチが必要だ。「ビルドが通ってエラーゼロなのに動かない」という状況に遭遇したら、まずsimctl log showでExtensionプロセスのログを確認すること。

Footnotes

  1. Smart Stack — Apple WatchのウィジェットスタックUI。複数のウィジェットを重ねて表示し、コンテキストに応じて最適なウィジェットを自動的に前面に出す。

  2. WidgetKit — Apple が提供するウィジェット開発フレームワーク。iOS 14/watchOS 9以降で使用可能。SwiftUIベースでウィジェットのUIを定義する。

  3. Widget Extension — WidgetKitウィジェットを提供するApp Extension。メインアプリとは独立したプロセスとして実行される。 2

  4. IntentConfiguration — WidgetKitで、ユーザーがカスタマイズ可能なウィジェットを定義するための構成型。SiriKit Intentと連携して設定画面を自動生成する。

  5. 文字列補間 — Swift の \(expression) 構文。文字列リテラル内に式を埋め込み、実行時に評価された値が文字列に変換される。

  6. LocalizedStringKey — SwiftUIのローカライズ対応文字列型。文字列リテラルから自動生成され、Localizable.stringsとの照合に使われる。文字列補間を含む場合は内部でフォーマット処理が発生する。

  7. Xcodeコンソール — Xcode下部のデバッグエリアに表示されるログ出力。print()やos_log()の出力、クラッシュ情報が表示されるが、デバッガがアタッチしているプロセスのログのみ。

  8. simctl — Xcodeに付属するシミュレータ制御コマンドラインツール。xcrun simctlで呼び出し、シミュレータの起動・アプリインストール・ログ取得・スクリーンショット等を実行できる。

  9. chronod — watchOSのシステムデーモン。ClockKit/WidgetKitのcomplicationやwidgetのライフサイクル管理、タイムライン更新のスケジューリングを担当する。

  10. complication descriptor — WidgetKitが watchOSに登録するウィジェットの定義情報。ウィジェットの種類、対応するファミリー(circular, rectangular等)、表示名を含む。

  11. companion app — Apple Watchアプリと対になるiPhoneアプリ。watchOSアプリの多くはiPhoneにcompanion appがインストールされていることを前提とする。Widget Extensionもcompanion appが必要。

  12. 文字列リテラル — ソースコード上に直接記述された固定文字列(例: "Hello")。変数や式の評価を含まない静的な値。