Swift 6 言語モードはコンパイラを「並行監査役」にします。@MainActor の誤用や Actor 越しの共有を code review で見つけるのではなく、xcodebuild 段階で失敗させます。そのため算力が安定し、バージョンを固定できるビルドノードが必要です。フルコンパイルができ、隣接テナントによる CPU 争奪でテールレイテンシが揺れない環境です。本記事では NUKCLOUD 独占リモート Mac ノードで Swift 6 厳格並行チェックを段階的に導入し、PR マージゲートに固定する方法を示します。
00なぜ今移行するのか——なぜ独占ノードが必要か
Swift 6 厳格並行チェック(-strict-concurrency=complete)が出すエラー数はコードベース規模に比例します。中規模 iOS プロジェクトで初回有効化時に 200〜800 件のコンパイルエラーが出ることも珍しくありません。修正は数週間から数ヶ月に及ぶため、その間 CI 環境は次を保証する必要があります:
- Xcode マイナーバージョンの固定:Actor isolation の診断文言は Xcode バージョンで変わるため、固定して初めて CI 出力を比較できます。
- 隣接テナントとの CPU 争奪なし:厳格並行のフルコンパイルは通常の debug ビルドより 20〜40% 長くなります。独占ノードで P95 テールレイテンシの暴騰を防ぎます。
- DerivedData の名前空間分離:複数 PR が並行チェックを同時実行する場合、増分キャッシュを分けないと結果が信頼できません。
013 段階の並行チェックレベルと移行パス
Swift には 3 段階のコンパイラフラグがあり、OTHER_SWIFT_FLAGS(Xcode Build Settings)または Package.swift の swiftSettings で設定できます:
| フラグ | 意味 | 推奨フェーズ |
|---|---|---|
-strict-concurrency=minimal | async/await の基本構文のみチェックし、Actor 境界はチェックしない | 既存コードのウォームアップ期(1〜2 週目) |
-strict-concurrency=targeted | Sendable、@MainActor など明示注釈のある型の Actor 境界をチェック | モジュール単位修正期(3〜6 週目) |
-strict-concurrency=complete | フルチェック。Swift 6 言語モードと同等。未処理の並行共有はすべてエラー | エラー 0 件達成後にゲートへ組み込み |
実務ではモジュール単位で段階的に進めるのがおすすめです。依存の少ないリーフモジュールから complete を有効にし、コアモジュールへ進めます。SwiftPM の .enableUpcomingFeature("StrictConcurrency") で target ごとのレベルを精密に制御できます。
02Actor 分離境界:3 類の高頻度エラー
移行期のコンパイルエラーの約 90% は次の 3 類に集中します。これらを先に片付けると赤文字の大半が消えます:
① Actor 越しの非 Sendable 型アクセス
// ❌ エラー: @MainActor 外から @MainActor 隔離状態へアクセス
actor DataFetcher {
func load() async {
let result = await networkClient.fetch()
// ❌ passing argument of non-sendable type 'UserModel' across actor boundary
await MainActor.run { viewModel.update(result) }
}
}
修正:UserModel を Sendable に準拠させる(値型は多くの場合自動)、または境界を越える変換を Actor 内で完了させます。
② @MainActor の付け忘れ
UIKit/SwiftUI の ViewController、ObservableObject サブクラスは移行前は暗黙にメインスレッドでした。Swift 6 では明示的な @MainActor が必要です。Xcode の「Fix」で一括修正できますが、UI 以外のロジックまで引き込まないよう各箇所を review してください。
③ グローバル変数への並行アクセス
// ❌ Swift 6 ではグローバルは Sendable または nonisolated が必要
var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ 移行期は nonisolated(unsafe) で明示
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ 推奨: Actor に包み async API で公開
actor CacheStore {
private let cache = NSCache<NSString, AnyObject>()
func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}
03リモート Mac で CI ゲートを設定する
PR ゲートに厳格並行チェックを組み込む要点:通常テスト Job とは別 Job で並行チェックを実行する。NUKCLOUD では 料金ページでディスクとリージョンを選び、注文ページで SSH と Runner 基線を受け取ってください。
name: Swift 6 Concurrency Gate
on:
pull_request:
branches: [main, release/*]
jobs:
concurrency-check:
runs-on: self-hosted # NUKCLOUD 専用リモート Mac Runner
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Xcode を選択(マイナーバージョン固定)
run: sudo xcode-select -s /Applications/Xcode_16.3.app
- name: Restore DerivedData cache
uses: actions/cache@v4
with:
path: ~/DerivedData/swift6-check
key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
restore-keys: swift6-${{ runner.name }}-
- name: Build with strict concurrency (complete)
run: |
xcodebuild build \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath ~/DerivedData/swift6-check \
OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
| xcbeautify --renderer github-actions
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: swift6-build-log-${{ github.sha }}
path: ~/DerivedData/swift6-check/Logs/Build/
-derivedDataPath ~/DerivedData/swift6-check を通常ビルドと分離し、並行チェックの成果物がデバッグキャッシュを汚染しないようにします。複数 PR が同時実行する場合は runner.name や PR 番号でさらに分離します。046 ステップ段階的移行 Runbook
-
01
ベースラインスナップショット:独占リモート Mac で
-strict-concurrency=minimalのフルビルドを 1 回実行し、エラー数とコンパイル時間を記録して移行起点の定量基準にします。 -
02
リーフモジュール先行:SwiftPM 依存グラフから下流依存のない target を選び、
completeを 1 つずつ有効化。target ごとに別 PR を出し、diff をレビュー可能に保ちます。 -
03
CI ブランチ戦略:Git の
feature/swift6-migration長期ブランチで並行チェック Job をwarn-only(continue-on-error: true)にし、日常 PR のマージは阻害しません。モジュールが 0 エラーになった時点でのみ、そのモジュールのチェックを block-merge ゲートへ昇格させます。 -
04
Xcode バージョンの固定:
xcode-selectのバージョンを Runbook と README に記載し、ローカル Mac 開発者と CI ノードが同一マイナーバージョンを使い、「ローカルは通るが CI は落ちる」ノイズを防ぎます。 -
05
エラー推移ダッシュボード:各 CI 実行の Swift 6 エラー数を構造化ログまたは Build Metrics に記録し、折れ線で移行進捗を追跡。チームに「今週 X 件減らす」週次目標を設定します。
-
06
フルゲート受け入れ:全モジュールが 0 エラーになったら、
mainの Branch Protection で「Require swift6-concurrency-check to pass」を有効化し、全 target の Build Settings にSWIFT_VERSION = 6を書き込み、正式に Swift 6 言語モードへ切り替えます。
05ローカル Mac と独占リモートノードの対照
Swift 6 移行期は開発者のローカル高速試行とCI のフル量信頼検証の両方が必要で、ノード要件は異なります:
| 次元 | 開発者ローカル Mac | NUKCLOUD 独占リモート Mac(CI) |
|---|---|---|
| Xcode バージョン | 開発者が各自管理、差異が出やすい | スクリプトでマイナー固定、チーム全体で一致 |
| DerivedData | 日常開発と共有、増分キャッシュが汚染される可能性 | 独立パス、PR または Runner 単位でバケット分け |
| CPU 可用性 | 他アプリと争奪、コンパイル時間にばらつき | ベアメタル独占、P95 レイテンシが予測可能 |
| 結果の信頼性 | ローカル合格 ≠ CI 合格 | ゲート段階の最終裁定環境 |
| 向いている用途 | 単一モジュールの素早い試行と Xcode 診断の読解 | フル並行チェックゲートと日次回帰 |
06よくある質問
-strict-concurrency=complete はコンパイラフラグで、Swift 5 言語モードのまま有効化でき、切り替え前にすべてのエラーを確認できます。正式な Swift 6 言語モード(SWIFT_VERSION = 6)はさらにレガシー構文を削除します。まず complete フラグでエラーを解消し、その後言語モードへ切り替えることを推奨します。Package.swift で .unsafeFlags(["-strict-concurrency=minimal"]) を使い特定依存のチェックを緩和するか、Podfile の post_install で SWIFT_STRICT_CONCURRENCY = minimal を設定できます。主要ライブラリの多くは 2026 年までに移行を完了しています。まず各ライブラリの Release Notes を確認してください。--concurrent-jobs N を設定し、DerivedData を PR 番号でバケット分け(-derivedDataPath ~/DerivedData/pr-${{ github.event.number }})すれば、各 Job は独立してコンパイルします。独占ベアメタルの強みはここにあります。隣接テナントとの CPU 争奪がなく、並行 Job の完了時間のばらつきが小さいです。-strict-concurrency=complete ゲートには NUKCLOUD マルチリージョン裸金属 Mac / クラウド Mac ノード が安定し、コンソール Runbook の 6 ステップ受け入れと揃えやすいです。