在雲端 Mac 建立 Swift 6 並行遷移檢查

在雲端 Mac 建立 Swift 6 並行遷移檢查

一個維護多年的 iOS 專案切換到 Swift 6 時,最危險的做法不是改得太慢,而是一次啟用語言模式後,才發現數百條並行診斷與業務變更混在一起。更穩妥的方式,是先在雲端 Mac 建立獨立檢查工作:固定工具鏈、隔離建置目錄、記錄目前的警告,再依模組逐步收緊規則。

先把遷移拆成兩個階段

第一階段仍使用 Swift 5 語言模式,但將 SWIFT_STRICT_CONCURRENCY 設為 complete。這個步驟會揭露跨隔離域傳遞非 Sendable 型別、主執行緒隔離呼叫,以及不安全的全域可變狀態,但不會立刻引入 Swift 6 的全部語意變更。

第二階段才將已完成清理的模組切換至 Swift 6。建議先從相依性較少的基礎套件開始,再處理網路層、資料層與介面層。葉節點模組先穩定後,上層的診斷通常也會隨之減少。

階段 語言模式 檢查設定 合併條件
發現問題 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"

projectscheme 與目標平台應由儲存庫設定提供,不要在多份指令碼中重複維護。如果專案使用 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;必須共享的同步物件,則應明確封裝鎖與存取邊界。

介面狀態應透過 @MainActor 隔離至具體型別或方法。不要為了快速通過檢查,就將整個資料層或所有協定都標示為 @MainActor;這會把背景工作隱性拉回主執行緒,並產生更多跨域呼叫。

@unchecked Sendable 只適用於安全性已由內部鎖、不可變設計或序列佇列保障的型別。程式碼審查至少要回答三個問題:保護了哪些欄位、所有讀寫是否都經過同一邊界,以及未來新增的欄位要如何繼續受到保護。如果無法回答,就不應使用這項宣告。

將回呼橋接至 async 時,還必須驗證 continuation 只會恢復一次。測試應同時涵蓋「沒有回呼」與「重複回呼」,不能只修到編譯器不再提示為止。

依模組切換並保留驗收證據

每個模組切換前,先在 Swift 5 complete 模式下將自身警告降至零,再啟用 Swift 6。驗收至少應包含 Debug 建置、Release 建置、單元測試與關鍵整合測試;只執行模擬器 Debug 建置,不足以發現最佳化設定或條件式編譯分支中的問題。

建議為每次遷移記錄模組名稱、負責人、切換提交、剩餘豁免項目與結果套件路徑。如果升級 Xcode,應先在獨立分支重新建立基準,不要將工具鏈升級、並行修正與業務功能塞進同一項變更。

遷移完成後,仍應保留嚴格檢查工作。Swift 6 能提高預設限制,但相依套件更新、Objective-C 邊界與產生程式碼,仍可能重新引入隔離缺口。穩定的最終狀態不是「曾經歸零」,而是每次合併都能證明沒有倒退。

常見問題

應該一次把整個專案切換到 Swift 6 嗎?

不建議。先在 Swift 5 模式啟用 complete 並行檢查,清理底層模組與共享狀態,再逐一切換目標。

可以大量使用 @unchecked Sendable 消除警告嗎?

不可以。只有在型別已透過不可變資料、鎖或序列隔離確保安全,並完成專門審查時才適合使用。

舊專案如何加入門禁而不阻塞開發?

先把目前警告數設為預算,只阻止新增警告;每完成一批修正就降低預算,直到允許值歸零。

獨享實體 Mac mini

依任務長度選擇雲端 Mac 租用週期

兩種 M4 設定可按日、週、月或季租用;節點與實際可用資訊以控制台即時回傳內容為準。

選擇設定並訂購