Claude CodeでSwift 6のstrict concurrencyエラーを段階的に直す手順
Swift 6の警告を1ターゲットずつ警告からエラーへ上げ、Claude Codeに直させてxcodebuildとStop hookで検証する流れ。CLAUDE.mdの規約例つき。
Swift 6の言語モード(Swift 6 language mode)に切り替えた途端、Sendable や @MainActor に関する診断が数百件並ぶことがあります。Swiftの移行ガイドも「数百、場合によっては数千の警告は珍しくない」と書いています。これをClaude Codeに丸ごと渡すと、ビルドは通っても @unchecked Sendable だらけのコードが返ってきがちです。
この記事では、モジュール(ターゲット)単位で検査を段階的に強め、Claude Codeには1ターゲットずつ直させる手順を扱います。検証には xcodebuild を使い、直し忘れはStop hookで拾います。
3段階で検査を強める
移行ガイドが示す進め方は、いきなりSwift 6モードに上げず、Swift 5モードのまま検査だけを強めていく方法です。警告のうちはビルドもテストも壊れないので、Claude Codeが途中で試行錯誤しても作業が止まりません。
ターゲット1つあたりの流れ
- 1
外側のターゲットを選ぶ
どのターゲットからも依存されていない一番外側のモジュールから始めます。変更の影響がそのターゲット内に収まるためです。
- 2
Swift 5のまま検査を強める
SWIFT_STRICT_CONCURRENCY = completeを設定し、データ競合の検査を警告として出します。 - 3
警告をゼロにする
Claude Codeに警告を直させ、
xcodebuildで確かめます。 - 4
Swift 6モードに上げる
SWIFT_VERSION = 6に切り替えます。残りの診断はエラーとして現れます。
外側から始めるのは移行ガイドの推奨です。ただし、自分が管理する依存先にグローバルな可変状態や、暗黙に Sendable となる型があれば、それが多数の警告の根本原因になっていることもあります。その場合は依存先を先に直したほうが、警告の総数は早く減ります。
設定の置き場所はxcconfigにする
Xcodeでは、ビルド設定の「Strict Concurrency Checking」を「Complete」にします。xcconfigファイルなら次の1行です。
// App.xcconfig
SWIFT_STRICT_CONCURRENCY = complete;言語モードの切り替えは、ビルド設定の「Swift Language Version」を「6」にするか、xcconfigで SWIFT_VERSION = 6; と書きます。ターゲットごとにxcconfigを分けておけば、「このターゲットだけ検査を強める」「このターゲットだけSwift 6にする」という操作が、設定ファイルの1行の差分で済みます。Claude Codeに渡す指示も「Core.xcconfig の設定を変える」と具体的になります。
Swift Package Managerのターゲットは設定が少し違います。swift-tools-version: 6.0 のマニフェストは全ターゲットをSwift 6モードにするので、移行が済んでいないターゲットには .swiftLanguageMode(.v5) を付けて旧モードに留めます。6.0以降のツールでSwift 5モードのまま検査だけ強めるには、.enableUpcomingFeature("StrictConcurrency") を使います。
| 段階 | Xcode(xcconfig) | SwiftPM(ターゲット設定) | 診断の扱い |
|---|---|---|---|
| 0. 現状 | Xcode(xcconfig)既定 | SwiftPM(ターゲット設定).swiftLanguageMode(.v5) | 診断の扱い検査なし |
| 1. 警告で確認 | Xcode(xcconfig)SWIFT_STRICT_CONCURRENCY = complete | SwiftPM(ターゲット設定).enableUpcomingFeature("StrictConcurrency") | 診断の扱い警告 |
| 2. Swift 6 | Xcode(xcconfig)SWIFT_VERSION = 6 | SwiftPM(ターゲット設定).v5 を外す | 診断の扱いエラー |
先に1機能だけ試す手もある
警告が多すぎるときは、complete の前に個別の機能から入れる選択肢があります。移行ガイドは DisableOutwardActorInference(SE-0401)、GlobalConcurrency(SE-0412)、InferSendableFromCaptures(SE-0418)の3つを、独立に任意の順で有効にできる機能として挙げています。1種類の問題に集中できるので、Claude Codeへの指示も「グローバル変数の警告だけを直す」と絞れます。
ビルド確認をコマンド1本にする
Claude Codeが「直った」と判断する根拠を、自分の推測ではなくコンパイラの出力にします。xcodebuild は、末尾に 設定名=値 を渡すと、その実行に限りビルド設定を上書きできます。
xcodebuild -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build SWIFT_STRICT_CONCURRENCY=complete -quiet-quiet は警告とエラー以外を出力しないオプションです。診断だけが返るので、Claude Codeが読む文脈も小さく済みます。ターゲット名を指定して単体で回すなら -target を使います。xcconfigに設定を書いた場合は、末尾の上書きは不要です。
Bash(xcodebuild *) の許可ルールの書き方と、確認ダイアログを減らす設定はClaude CodeとXcodeでSwiftUI iOSアプリを開発する手順にまとめています。
CLAUDE.mdに直し方の規約を書く
規約なしで依頼すると、@unchecked Sendable を付けて黙らせる直し方が選ばれやすくなります。移行ガイドの指針は「いま真であることを表現する(express what is true now)」です。コードを大きく組み替えずに警告ゼロの状態まで進み、使った安全でない回避策を、あとで見直す目印にします。これをそのままCLAUDE.mdに書きます。
## Swift 6 移行ルール
- 対象は依頼されたターゲットだけ。他ターゲットの設定とコードは触らない
- リファクタリングはしない。現状の実態を型と分離境界で表現する
- 回避策(@unchecked Sendable / @preconcurrency)を使うときは、
その行に `// TODO(swift6): 理由` を書く
- 完了条件: 対象ターゲットで `xcodebuild ... -quiet` の出力が空になること回避策の使いどころは、診断の種類で分けると決めやすくなります。
| 診断の例 | 直し方の候補(移行ガイドの記載) |
|---|---|
var のグローバル変数が「not concurrency-safe」 | 直し方の候補(移行ガイドの記載)let にする / @MainActor に限定する / 外部の同期機構で守られているときだけ安全でないと明示する |
非 Sendable 値が分離境界を越える | 直し方の候補(移行ガイドの記載)sending 引数にする(自分が管理しない型にも使える) |
| 未対応の依存モジュールの型 | 直し方の候補(移行ガイドの記載)@preconcurrency import で診断を弱める |
| 手動で同期している参照型 | 直し方の候補(移行ガイドの記載)@unchecked Sendable(根拠をコメントに残す) |
グローバル変数の診断は、コンパイラ自身が3つの対処候補をnoteとして出します。表の1行目はその3候補です。@unchecked Sendable を後付けで付ける再適合(retroactive conformance)は、ガイドが「極めて慎重に」と注意している手法です。特に公開APIの型に使うのは避けます。
回避策を拾って次の宿題にする
ターゲットが警告ゼロになったら、回避策の一覧をClaude Codeに作らせます。
Sources/Core 以下で、@unchecked Sendable と @preconcurrency を
使っている行を grep で洗い出し、TODO(swift6) コメントの理由と一緒に
表にして。コードは変更しないでこの表が、次のリファクタリングの対象です。移行ガイドも、回避策は後でより安全な分離方式へ移す手がかりになると位置づけています。
1ターゲット分の依頼文
設定とCLAUDE.mdが揃ったら、依頼は1ターゲット分だけに絞ります。次のような文面が一例です。
Core ターゲットを移行して。Core.xcconfig に
SWIFT_STRICT_CONCURRENCY = complete を足し、xcodebuild で警告を
確認。警告が出なくなるまで直して。直し方は CLAUDE.md の
「Swift 6 移行ルール」に従い、他ターゲットは触らないことこの文面なら、設定変更、検証コマンド、守るルールの3点が揃います。最初の依頼で警告の件数を数えさせ、種類別の内訳(グローバル変数、Sendable、@MainActor など)を出させてから直し始める進め方も取れます。件数が減っていく様子は、次の依頼の範囲を決める判断材料になります。
直し忘れをStop hookで拾う
Claude Codeが自己申告で作業を終えても、最後にビルドを通した保証にはなりません。Stopフックを使えば、止まる直前にビルドを走らせ、警告やエラーが残っていれば継続させられます。
Stopフックは、応答が終わるたびに発火するフックです。exit 2で、Claudeが止まるのを防いで会話を続けさせます。stderrには理由を書きます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/swift-check.sh"
}
]
}
]
}
}.claude/hooks/swift-check.sh は次のとおりです。
#!/bin/bash
INPUT=$(cat)
# 継続中の再発火では止めない(無限ループ防止)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(xcodebuild -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build -quiet 2>&1)
ISSUES=$(echo "$OUT" | grep -E '(warning|error):' | sort -u | head -30)
if [ -n "$ISSUES" ]; then
echo "Swiftのビルドに警告またはエラーが残っています:" >&2
echo "$ISSUES" >&2
exit 2
fi
exit 0stop_hook_active の確認は必須です。Stopフックが連続して8回ブロックすると、Claude Codeは上書きして止まり、警告を出します。このスクリプトは継続中の再発火では素通りさせるので、連続ブロックは1回で終わり、この上限には届きません。1回の停止につき追加の作業を促すのは1ラウンドだけです。1ラウンドで片付かない規模なら、依頼をファイル単位に分けると確実です。
フックのタイムアウトは、command 型の既定が10分です。大きなアプリで10分を超えるビルドなら、timeout を秒単位で指定します。フックの全体像はClaude Code hooksでテストを自動実行するの使い分け表も参考になります。
PostToolUseは重いビルドに向かない
編集のたびに検査したくなりますが、PostToolUse で xcodebuild を回すと、1回の依頼で何度もビルドが走ります。PostToolUseは「ツールが成功した後」に発火し、すでに実行された操作を取り消せません。軽量なLintなら向きますが、ビルドはLint自動修正のPostToolUse hookに任せ、Stopに寄せるほうが時間を節約できます。
警告の切り分け
診断が出たとき、どこから手を付けるかの判断は次のとおりです。
診断が出たときの確認順
- Qその型・変数は自分が管理するコードか?答えがいいえ(依存先)答えがはい
依存先が未対応なら、
@preconcurrency importで診断を弱めます。 - Qグローバル変数や静的変数で、後から書き換わるか?答えがいいえのとき答えがはい
letにできないか確かめます。できないなら@MainActorに限定します。 - Q値が別の分離境界へ渡されているか?答えがいいえのとき答えがはい
Sendable適合かsending引数を検討します。
それでも解決しない
回避策を使い、TODO(swift6) コメントで理由を残します。
Claude Codeにこの順序をそのまま指示すれば、@unchecked Sendable に最初に手が伸びることを防げます。
よくあるつまずき
落とし穴
警告ゼロにした後でSwift 6に上げたらエラーが増えた
Swift 6モードでは、完全検査で警告だった問題の一部がエラーになります。移行ガイドも、Swift 6モードを有効にすると潜在的な問題の一部がエラーになると説明しています。Swift 6に上げたら、もう一度ターゲット単位で同じ手順を回します。
依存ターゲットの設定を勝手に変えられる
Claude Codeは、エラーの原因が依存先にあると見ると、依存先の設定やコードも直そうとします。CLAUDE.mdで「対象は依頼されたターゲットだけ」と範囲を決めておくと、他ターゲットの変更を防げます。依存先の移行は、次のターゲットとして別の依頼にします。
警告が多すぎて文脈が埋まる
診断が数百件あると、出力が大きくなります。フックの出力はスクリプト側で head -30 のように絞っています。hooksの標準出力や additionalContext も、文字数に上限(10,000文字)があります。件数が多いときは、ファイル単位に分けて依頼するほうが、確実に1つずつ潰せます。
まとめ
作業の単位は、ターゲット1つ、診断の種類1つ、依頼1回です。設定はxcconfigの1行差分で戻せるようにしておき、完了の判定は xcodebuild -quiet の出力が空になることに置きます。CLAUDE.mdには、回避策にTODOを付ける約束を書いておきます。
この約束が効くのは、移行の最後です。TODO(swift6) の付いた行が、次に分離境界を設計し直す場所の一覧になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。