一個維護多年的 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"
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;必須共享的同步物件,則應明確封裝鎖與存取邊界。
介面狀態應透過 @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 租用週期
兩種 M4 設定可按日、週、月或季租用;節點與實際可用資訊以控制台即時回傳內容為準。