独占リモート Mac で Swift 6 を移行:厳格並行 CI ゲートと Actor 分離 Runbook

Swift 6 は並行の正しさを実行時クラッシュからコンパイル時エラーへ前倒しします。-strict-concurrency=complete を有効にすると、データ競合と Actor 境界違反はビルド失敗になります。本記事では、NUKCLOUD 独占リモート Mac ノードでこのフラグを CI ゲートに組み込む方法と、マージ前に並行コンプライアンスを通す 6 ステップ段階的移行チェックリストを示します。

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 が並行チェックを同時実行する場合、増分キャッシュを分けないと結果が信頼できません。
ヒント:CI が「共有分単位」ホスト型 macOS を使っている場合、Swift 6 厳格並行ゲートを入れる前にキュー P95 を評価してください。ノードの意味づけは NUKCLOUD 独占 Apple Silicon ノード Runbook を参照。

013 段階の並行チェックレベルと移行パス

Swift には 3 段階のコンパイラフラグがあり、OTHER_SWIFT_FLAGS(Xcode Build Settings)または Package.swiftswiftSettings で設定できます:

フラグ意味推奨フェーズ
-strict-concurrency=minimalasync/await の基本構文のみチェックし、Actor 境界はチェックしない既存コードのウォームアップ期(1〜2 週目)
-strict-concurrency=targetedSendable@MainActor など明示注釈のある型の Actor 境界をチェックモジュール単位修正期(3〜6 週目)
-strict-concurrency=completeフルチェック。Swift 6 言語モードと同等。未処理の並行共有はすべてエラーエラー 0 件達成後にゲートへ組み込み

実務ではモジュール単位で段階的に進めるのがおすすめです。依存の少ないリーフモジュールから complete を有効にし、コアモジュールへ進めます。SwiftPM の .enableUpcomingFeature("StrictConcurrency") で target ごとのレベルを精密に制御できます。

02Actor 分離境界:3 類の高頻度エラー

移行期のコンパイルエラーの約 90% は次の 3 類に集中します。これらを先に片付けると赤文字の大半が消えます:

① Actor 越しの非 Sendable 型アクセス

Swift — 典型的なエラー例
// ❌ エラー: @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) }
    }
}

修正:UserModelSendable に準拠させる(値型は多くの場合自動)、または境界を越える変換を Actor 内で完了させます。

② @MainActor の付け忘れ

UIKit/SwiftUI の ViewControllerObservableObject サブクラスは移行前は暗黙にメインスレッドでした。Swift 6 では明示的@MainActor が必要です。Xcode の「Fix」で一括修正できますが、UI 以外のロジックまで引き込まないよう各箇所を review してください。

③ グローバル変数への並行アクセス

Swift — グローバル変数の修正例
// ❌ 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 基線を受け取ってください。

GitHub Actions — .github/workflows/swift6-concurrency-check.yml
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/
DerivedData のバケット分け:-derivedDataPath ~/DerivedData/swift6-check を通常ビルドと分離し、並行チェックの成果物がデバッグキャッシュを汚染しないようにします。複数 PR が同時実行する場合は runner.name や PR 番号でさらに分離します。

046 ステップ段階的移行 Runbook

  1. 01
    ベースラインスナップショット:独占リモート Mac で -strict-concurrency=minimal のフルビルドを 1 回実行し、エラー数とコンパイル時間を記録して移行起点の定量基準にします。
  2. 02
    リーフモジュール先行:SwiftPM 依存グラフから下流依存のない target を選び、complete を 1 つずつ有効化。target ごとに別 PR を出し、diff をレビュー可能に保ちます。
  3. 03
    CI ブランチ戦略:Git の feature/swift6-migration 長期ブランチで並行チェック Job を warn-onlycontinue-on-error: true)にし、日常 PR のマージは阻害しません。モジュールが 0 エラーになった時点でのみ、そのモジュールのチェックを block-merge ゲートへ昇格させます。
  4. 04
    Xcode バージョンの固定:xcode-select のバージョンを Runbook と README に記載し、ローカル Mac 開発者と CI ノードが同一マイナーバージョンを使い、「ローカルは通るが CI は落ちる」ノイズを防ぎます。
  5. 05
    エラー推移ダッシュボード:各 CI 実行の Swift 6 エラー数を構造化ログまたは Build Metrics に記録し、折れ線で移行進捗を追跡。チームに「今週 X 件減らす」週次目標を設定します。
  6. 06
    フルゲート受け入れ:全モジュールが 0 エラーになったら、main の Branch Protection で「Require swift6-concurrency-check to pass」を有効化し、全 target の Build Settings に SWIFT_VERSION = 6 を書き込み、正式に Swift 6 言語モードへ切り替えます。

05ローカル Mac と独占リモートノードの対照

Swift 6 移行期は開発者のローカル高速試行CI のフル量信頼検証の両方が必要で、ノード要件は異なります:

次元開発者ローカル MacNUKCLOUD 独占リモート Mac(CI)
Xcode バージョン開発者が各自管理、差異が出やすいスクリプトでマイナー固定、チーム全体で一致
DerivedData日常開発と共有、増分キャッシュが汚染される可能性独立パス、PR または Runner 単位でバケット分け
CPU 可用性他アプリと争奪、コンパイル時間にばらつきベアメタル独占、P95 レイテンシが予測可能
結果の信頼性
ローカル合格 ≠ CI 合格ゲート段階の最終裁定環境
向いている用途単一モジュールの素早い試行と Xcode 診断の読解フル並行チェックゲートと日次回帰

06よくある質問

Swift 6 厳格並行チェックと「Swift 6 言語モード」は同じですか?
完全には同じではありません。-strict-concurrency=complete はコンパイラフラグで、Swift 5 言語モードのまま有効化でき、切り替え前にすべてのエラーを確認できます。正式な Swift 6 言語モード(SWIFT_VERSION = 6)はさらにレガシー構文を削除します。まず complete フラグでエラーを解消し、その後言語モードへ切り替えることを推奨します。
サードパーティ依存(CocoaPods / SPM)がまだ Swift 6 非対応の場合は?
Package.swift.unsafeFlags(["-strict-concurrency=minimal"]) を使い特定依存のチェックを緩和するか、Podfile の post_installSWIFT_STRICT_CONCURRENCY = minimal を設定できます。主要ライブラリの多くは 2026 年までに移行を完了しています。まず各ライブラリの Release Notes を確認してください。
NUKCLOUD ノードで複数 PR の並行チェックを同時実行できますか?
できます。Runner 登録時に --concurrent-jobs N を設定し、DerivedData を PR 番号でバケット分け(-derivedDataPath ~/DerivedData/pr-${{ github.event.number }})すれば、各 Job は独立してコンパイルします。独占ベアメタルの強みはここにあります。隣接テナントとの CPU 争奪がなく、並行 Job の完了時間のばらつきが小さいです。
料金とノード仕様はどこで確認できますか?
料金ページ注文ページヘルプセンターを参照してください。
分単位プール、自前 Mac、独占リモートノードの選び方は?
長期の -strict-concurrency=complete ゲートには NUKCLOUD マルチリージョン裸金属 Mac / クラウド Mac ノード が安定し、コンソール Runbook の 6 ステップ受け入れと揃えやすいです。