長年保守してきたiOSプロジェクトをSwift 6へ移行する際、最も危険なのは作業が遅れることではありません。言語モードを一度に切り替えた結果、数百件もの並行処理診断が機能変更に紛れ込むことです。より安全に進めるには、まずクラウドMac上に独立したチェックジョブを構築します。ツールチェーンを固定し、ビルドディレクトリを分離して現在の警告を記録したうえで、モジュール単位にチェックを段階的に厳格化します。
移行を最初に2段階へ分ける
第1段階ではSwift 5言語モードを維持したまま、SWIFT_STRICT_CONCURRENCYをcompleteに設定します。これにより、分離ドメインをまたいで渡される非Sendable型、メインスレッド分離された処理の呼び出し、安全でないグローバル可変状態が検出されます。一方で、Swift 6のセマンティクス変更がすべて即座に導入されるわけではありません。
第2段階で、問題の解消が完了したモジュールだけをSwift 6へ切り替えます。依存関係の少ない基盤パッケージから着手し、その後にネットワーク層、データ層、UI層を進めるのがおすすめです。リーフモジュールを先に安定させると、上位層の診断も通常は減っていきます。
| 段階 | 言語モード | チェック設定 | マージ条件 |
|---|---|---|---|
| 問題の検出 | Swift 5 | complete | 並行処理の警告が増加しないこと |
| モジュールの修正 | Swift 5 | complete | 対象モジュールの警告がゼロになること |
| 正式な切り替え | Swift 6 | デフォルトの厳格ルール | すべてのビルドとテストが成功すること |
並行処理移行ゲートの目的は、初日からすべての警告をなくすことではありません。初日以降、メインブランチに新たな問題を追加させないことです。
再現可能なチェックジョブを固定する
クラウドMac上のジョブでは、Xcode、Swiftコンパイラ、コミットの各バージョンを明示的に記録する必要があります。開発者の手動ビルドで残ったDerivedDataを再利用してはいけません。古いモジュールキャッシュによって、実際の依存関係が隠れる可能性があるためです。コミットごとに独立したディレクトリを使用し、ジョブの終了後に保持ポリシーに従って削除します。
#!/bin/zsh
set -o pipefail
export LC_ALL=C
root="$PWD/.ci"
derived="$root/DerivedData"
result="$root/ConcurrencyCheck.xcresult"
log="$root/concurrency.log"
rm -rf "$derived" "$result"
mkdir -p "$root"
xcodebuild -version
xcrun swiftc --version
git rev-parse HEAD
xcodebuild \
-project Example.xcodeproj \
-scheme Example \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$derived" \
-resultBundlePath "$result" \
SWIFT_VERSION=5.0 \
SWIFT_STRICT_CONCURRENCY=complete \
build 2>&1 | tee "$log"
build_status=${pipestatus[1]}
exit "$build_status"
project、scheme、対象プラットフォームはリポジトリの設定から与え、複数のスクリプトで重複管理しないようにします。プロジェクトがworkspaceを使用している場合は、-projectを-workspaceへ置き換えます。同時に、共有schemeがバージョン管理リポジトリへコミットされていることも確認してください。
警告予算で既存のメインブランチへ組み込む
長期間運用されてきたプロジェクトでは、警告をすぐにゼロにできないことが一般的です。初回実行後に並行処理診断の件数を保存し、それを初期予算として設定します。以降のコミットでは、件数が予算を超えた時点でジョブを失敗させます。問題を一まとまり修正するたびに、同じマージリクエスト内で予算も引き下げます。
budget=${CONCURRENCY_WARNING_BUDGET:-0}
count=$(grep -Ec \
'warning:.*(Sendable|actor-isolated|concurrency-safe)' \
.ci/concurrency.log || true)
printf 'concurrency warnings: %s, budget: %s
' "$count" "$budget"
test "$count" -le "$budget"
テキストマッチは移行期のゲートとして有効ですが、LC_ALL=Cを固定し、Xcodeのアップグレード時には診断メッセージの文言を再確認する必要があります。失敗時にはビルド結果バンドルを添付ファイルとして保存し、途中で切れたターミナルログしか残らない状況を避けます。最終的に予算がゼロになれば、並行処理に関する警告をすべて直接禁止できます。
生成コードによるノイズを避ける
APIクライアントやモデルをツールで生成している場合は、ジェネレータのバージョンをリポジトリ設定で固定します。すぐに修正できない外部の生成コードは、独立したターゲットモジュールへ分離できます。ただし、ビジネスロジックを含むモジュール全体を厳格なチェックから除外してはいけません。そうすると、新たに手書きされたコードまでゲートを迂回することになります。
一括で抑制せず問題の種類ごとに修正する
Sendable診断が出た場合は、まずそのデータを本当にタスク間で受け渡す必要があるかを判断します。読み取り専用の設定は、値型のスナップショットへ変更する方法を優先します。可変キャッシュを持つ参照型はactor内に置くことができます。共有が必須の同期オブジェクトについては、ロックとアクセス境界を明示的にカプセル化します。
UIの状態は、具体的な型またはメソッドに@MainActorを指定して分離します。チェックを手早く通すために、データ層全体やすべてのプロトコルへ@MainActorを付けてはいけません。バックグラウンド処理が暗黙的にメインスレッドへ戻され、分離ドメインをまたぐ呼び出しも増えてしまいます。
@unchecked Sendableが適しているのは、内部ロック、不変設計、またはシリアルキューによって安全性が保証されている型だけです。コードレビューでは、少なくとも3つの問いに答える必要があります。どのフィールドが保護されているか、すべての読み書きが同じ境界を通っているか、将来追加されるフィールドもどのように保護し続けるかです。答えられない場合、この宣言を使うべきではありません。
コールバックをasyncへブリッジする場合は、continuationが1回だけ再開されることも検証します。「コールバックされない場合」と「コールバックが重複する場合」の両方をテスト対象に含め、コンパイラの警告が消えただけで修正を完了してはいけません。
モジュール単位で切り替えて検証結果を残す
各モジュールを切り替える前に、まずSwift 5 completeモードでそのモジュールの警告をゼロにし、その後でSwift 6を有効にします。受け入れ確認には、少なくともDebugビルド、Releaseビルド、ユニットテスト、主要な統合テストを含めます。シミュレータ向けのDebugビルドだけでは、最適化設定や条件付きコンパイルの分岐に起因する問題を検出できません。
移行ごとに、モジュール名、担当者、切り替えコミット、残っている例外、結果バンドルのパスを記録することをおすすめします。Xcodeをアップグレードする場合は、独立したブランチで先にベースラインを作り直します。ツールチェーンのアップグレード、並行処理の修正、機能変更を1つの変更へ詰め込んではいけません。
移行完了後も厳格なチェックジョブは残します。Swift 6ではデフォルトの制約が強化されますが、依存関係の更新、Objective-Cとの境界、生成コードによって分離の抜けが再び持ち込まれる可能性があります。安定した完了状態とは「一度は警告がゼロになった」ことではなく、マージのたびに後退がないと証明できる状態です。
よくある質問
プロジェクト全体を一度にSwift 6へ切り替えるべきですか?
推奨しません。まずSwift 5モードでcompleteチェックを有効にし、下位モジュールを修正してからターゲットごとに切り替えます。
@unchecked Sendableで警告をまとめて消してもよいですか?
不変性、ロック、直列化された分離によって安全性を説明できる型だけに限定し、理由を記録して個別にレビューします。
既存プロジェクトでゲートを導入する方法は?
現在の警告数を予算として固定し、増加だけを失敗にします。修正するたびに予算を下げ、最終的にゼロへ移行します。
タスクの長さに合わせてクラウドMacのレンタル期間を選択
2種類のM4構成を日単位、週単位、月単位、または四半期単位でレンタルできます。ノードと実際の利用可能状況は、コンソールからリアルタイムで返される情報をご確認ください。