AI 工具 24 min read

write-swift skill 實測:5 次對照補出取消檢查,版本表落後 6.4

write-swift 是 Emil Kowalski 收在 MIT 授權 emilkowalski/skills 裡的 AI agent skill,用 16 節規則教 AI 照 Swift 6.2 之後的並行模型寫 Swift。我讓 Claude Code 寫同一個 SwiftUI 列表做 5 次對照,沒裝 skill 也會用 @concurrent,裝了才 3 次都補上取消檢查與 Mutex——但它把 6.3 當目前版本,swift.org 已是 6.4.1。

write-swift skill 實測:5 次對照補出取消檢查,版本表落後 6.4
本文目錄 · 9
懶人包
  • write-swift 是 Emil Kowalski 收在 emilkowalski/skills(MIT)裡的 AI agent skill,一份 396 行的 SKILL.md,16 節加一張速查表,教 AI 照 Swift 6.2 之後的並行模型寫 Swift,2026-08-21 加入 repo。
  • repo 在 2026-10-21 是 48,159 stars、2,785 forks,但這是 15 支 skill 共用的數字,不是 write-swift 一支的。
  • 我用同一句需求讓 Claude Code(Sonnet)寫 SwiftUI 文章列表,沒裝 skill 跑 2 次、裝了跑 3 次。沒裝的版本已經會用 @concurrent 和 actor 去重;裝了之後 3 次都多出 try Task.checkCancellation() 與 Mutex,2 次用上 @Test(arguments:)。
  • 裝了不點名,3 次裡有 2 次是模型自己載入(另 1 次是點名)。frontmatter 沒有 disable-model-invocation,靠 description 自動觸發。
  • 坑:它把 Swift 6.3 當「目前版本」,但 swift.org 現在是 6.4.1,標 ⚠ 未釋出的 6.4 功能已經能用;我對照 swift-evolution 另外找到 2 處版本標錯。程式碼都沒有在 Xcode 或裝置上編譯執行。
  1. write-swift skill 是什麼、解決什麼問題?
  2. 16 節規則各在講什麼?
  3. 哪些規則對應哪個 Swift 版本?版本表準不準?
  4. 它什麼時候會被觸發?怎麼點名?
  5. 怎麼安裝到 Claude Code?
  6. 實測:同一個 SwiftUI 列表,有沒有 skill 差在哪?
  7. 有哪些限制和坑?
  8. 適合誰、不適合誰?
  9. 延伸資源

🧭 write-swift skill 是什麼、解決什麼問題?

write-swift 是一份給 AI coding agent 讀的 SKILL.md,資料夾裡只有這一個檔。它在 2026-08-21 由 Emil Kowalski 加進 emilkowalski/skills,commit 8e1ec02 的訊息附了 Co-authored-by: Claude Opus 5。

它要處理的問題寫在第 3 節開頭:並行是 agent 最常寫錯的地方,因為 Swift 6.2 改了規則。舊規則下,函式標了 async 就可能離開目前的 actor;6.2 之後 async 函式預設留在呼叫它的 actor 上,要明確丟到背景得寫 @concurrent。模型的訓練資料大多是舊寫法,所以常見的輸出是到處加 @MainActor、用 Task.detached 或 DispatchQueue.global() 把工作推出去。

skill 的主軸是一句話:先寫最簡單、最靜態、單執行緒的版本,要動態能力(並行、參考型別、existential、unsafe pointer)時,得說得出理由才往下走。開頭有一張「預設值階梯」表,例如資料先用 struct/enum,執行先留在 main actor 同步跑,Instruments 量到卡頓才依序升級成 async、@concurrent、actor。

emilkowalski/skills 的 write-swift 對照實驗:5 次輸出的 grep 計數表,以及沒裝 skill 與裝了 skill 的縮圖程式碼並排

📚 16 節規則各在講什麼?

我把 16 節照主題分成五組,用自己的話轉述重點。原文是英文,要逐字看請開 repo 的 SKILL.md。

組別節次重點
資料與錯誤§1 值型別、§2 錯誤與 optional預設 struct/enum,class 只給有身分或共享狀態的東西;可恢復錯誤用 throw,寫錯程式用 precondition;typed throws 留給內部函式
並行§3 並行、§4 Sendable、§5 結構化並行、§6 SwiftUI 並行單執行緒 → async → @concurrent → actor 四步不跳級;actor 重入要防;資料競爭先「不共享」,最後才用 Mutex
抽象與 API§7 protocol 與泛型、§8 API 設計、§12 macros先寫具體型別,重複了再抽 protocol;預設 some P,要存異質集合才用 any P;不寫 get 前綴
效能與記憶體§9 效能、§10 ARC、§14 unsafe 與互通先改演算法再 profile;final、InlineArray、Span;物件壽命在最後一次使用就結束,不是到大括號
工具與遷移§11 Swift Testing、§13 Logger、§15 現代語法表、§16 遷移新測試用 Swift Testing,XCTest 只剩 UI 自動化、效能量測與 Objective-C;Logger 取代 print;Swift 6 遷移一個 target 一個 target 做

並行那四節是全份的重心

第 3 節的設定建議值得單獨拿出來講。它要所有專案開 Approachable Concurrency;app 與 UI 模組再把 Default Actor Isolation 設成 MainActor,SwiftPM 的寫法是 .defaultIsolation(MainActor.self)。但它也明說通用 library 不要開 main-actor-by-default,library 的 API 應該是 nonisolated,讓呼叫端決定在哪裡跑。

actor 重入那段講的是一個具體 bug:檢查快取 → await 下載 → 寫回快取。兩個 task 同時 miss,就會各下載一次,後寫的蓋掉先寫的。解法是 await 之後重新檢查,或把進行中的下載去重。

第 4 節有一句反直覺的話:多數 model class 既不該是 @MainActor,也不該是 Sendable。刻意保持 non-Sendable,才不會出現半個 model 在主執行緒改、另一半在背景改的情況。

第 5 節把取消講得很直白:取消只是設一個旗標,什麼都不會停下來。要在開始昂貴工作之前檢查 Task.isCancelled 或 try Task.checkCancellation()。

第 6 節提醒 View 本身就是 @MainActor,裡面的 @State 也是,所以 view 和 view model 幾乎不用自己寫 @MainActor。

第 15 節是給 agent 的「別再寫舊寫法」清單

這節是一張表,左欄是 agent 常寫的舊寫法,右欄是新寫法和起始版本。例如用 @Observable 取代 ObservableObject、用 InlineArray 取代熱路徑上的固定大小陣列、用 raw identifier 寫測試名稱(`@Test func fruits have a tropical climate()`)。版本欄的準確度是下一節要查的。

🔢 哪些規則對應哪個 Swift 版本?版本表準不準?

skill 開頭寫著「Toolchain baseline: Swift 6.3 (current release as of August 2026)」,並把 6.4 的功能標成 ⚠ 未釋出。我在 2026-10-21 查了兩個地方:

  • swift.org 的安裝頁顯示目前版本是 6.4.1,swift.org blog 的「Swift 6.4 Released」日期是 2026-09-15。
  • 每個功能對應的 swift-evolution 提案頁,看 Status 欄寫的是哪一版實作。
swift.org 安裝頁,右上角按鈕顯示 Install (6.4.1)
skill 裡的寫法skill 標的版本swift-evolution 狀態(2026-10-21 查)判斷
async 留在呼叫端、@concurrent(SE-0461)6.2Implemented (Swift 6.2)正確
Default actor isolation(SE-0466)6.2Implemented (Swift 6.2)正確
型別上的 nonisolated(SE-0449)6.1Implemented (Swift 6.1)正確
InlineArray(SE-0453)、Span(SE-0447)、Observations(SE-0475)6.2皆為 Implemented (Swift 6.2)正確
module selector ::(SE-0491)、@c(SE-0495)、@inline(always)(SE-0496)6.3皆為 Implemented (Swift 6.3)正確
raw identifiers(SE-0451)6.0Implemented (Swift 6.2)標錯
weak let(SE-0481)6.4 ⚠Implemented (Swift 6.3)標錯
cancellation shield(SE-0504)、borrow/mutate accessors(SE-0507)、~Sendable(SE-0518)、@diagnose(SE-0522)、編譯期檢查的 Continuation(SE-0528)6.4 ⚠ 未釋出皆為 Implemented (Swift 6.4)已過期:6.4 已釋出
mapKeyedValues(SE-0510)6.4 ⚠Accepted with modifications我查不到它進了哪一版,6.4 的發布文沒有提到
@available(anyAppleOS ...)6.4 ⚠沒有 evolution 提案swiftlang/swift 原始碼的診斷文件裡有它,我沒確認是哪一版開始有
raw identifiers 標錯的影響最實際。照 skill 的表,toolchain 是 6.0 或 6.1 的專案也會被鼓勵寫 ` func 名稱有空白() `,但這個語法要 6.2 才有。

另一個我沒辦法驗證的說法:skill 寫 MainActor 預設隔離「是 Xcode 26 新 app 專案的預設值」。Swift 6.2 的發布文只介紹這個選項,沒有提到 Xcode 的預設值;這台是 Windows,我沒有 Xcode 26 可以開新專案確認。

🎯 它什麼時候會被觸發?怎麼點名?

frontmatter 只有 name 和 description 兩個欄位,沒有 disable-model-invocation,所以模型可以自己決定要不要載入。description 列的觸發情境是:寫、審查或遷移 Swift,或者要修並行錯誤、卡頓、資料競爭、retain cycle、效能問題。

要確定它會被用上,就在需求前面打 /write-swift:

/write-swift 幫我把這個 ViewModel 改成 Swift 6.2 的 approachable concurrency 寫法

skill 第 8–14 行有一段「Initial Response」:如果被叫起來時沒有附問題,只回一句「I'm ready to help you write modern Swift, the way the language wants to be written.」,然後等你發問。我點名那次 /write-swift 後面直接接了需求,模型沒有回這句,直接開始寫程式。

📦 怎麼安裝到 Claude Code?

官方 README 給的是 skills 這支 CLI。我在 2026-10-21 用的版本是 1.7.4:

npx -y skills@latest --version
npx -y skills@latest add emilkowalski/skills --skill write-swift --agent claude-code --copy -y

驗證方式是看專案底下有沒有長出 skill 檔:

ls .claude/skills/write-swift/
# 預期只有 SKILL.md 一個檔

我這次跑完 exit code 是 0,產生 .claude/skills/write-swift/SKILL.md(跟 repo 版逐字相同)和一份 skills-lock.json。安裝過程顯示的三家安全評估是 Gen Safe、Socket 0 alerts、Snyk Low Risk。

--agent claude-code 和 --copy 是我為了不跳互動選單加的;不加的話 CLI 會問你要裝給哪個 agent、要用 symlink 還是複製。

🧪 實測:同一個 SwiftUI 列表,有沒有 skill 差在哪?

實驗設計

這台是 Windows,swift --version 回 command not found。我沒有裝 Swift toolchain,所以這次只做原始碼層的對照:以下所有程式碼都沒有在 Xcode 或裝置上編譯執行,我比的是寫法,不是能不能跑。

需求是同一句話,要求用 Swift 6.2、iOS 26、Xcode 26 新專案的預設設定寫兩個檔:

  • ArticleList.swift:進畫面用 URLSession 非同步載入文章清單、下拉重新整理、離開畫面要取消載入;縮圖下載後縮成 120×120,縮圖很吃 CPU 不能卡主執行緒,同一張不重複下載;要有載入中、錯誤、空清單三種狀態。
  • ArticleListTests.swift:載入邏輯的單元測試。
執行方式是 claude -p 搭 --model sonnet --dangerously-skip-permissions,Claude Code 2.1.306,實際模型是 claude-sonnet-5-5。每組一個獨立資料夾:
組別設定行數(兩檔)時間費用
noskill沒裝 skill46265.6 秒0.30 美元
noskill2沒裝 skill(重跑)55999.9 秒0.41 美元
auto1裝了,沒點名501125 秒0.47 美元
auto2裝了,沒點名399108 秒0.42 美元
named裝了,開頭 /write-swift31177.6 秒0.39 美元
noskill 兩個檔都寫完了,但最後的回覆是額度用完的訊息,所以我補跑一次 noskill2 當乾淨的基準。auto1、auto2 的紀錄裡都有模型主動呼叫 Skill 工具載入 write-swift;兩組 noskill 的可用 skill 清單裡沒有它。

沒裝 skill 的版本已經很現代

這是我最意外的地方。兩組 noskill 都用了 @concurrent 把縮圖處理丟到背景、用 actor 加「進行中 Task 字典」去重、用 @Observable 和 Swift Testing,五組裡沒有任何一組用 Task.detached、DispatchQueue 或 XCTest。Sonnet 本身就知道 6.2 的寫法,skill 沒有從零教它。

裝了 skill 之後穩定多出來的東西

差異集中在細節,而且剛好都對得上 skill 的條文:

規則(skill 出處)noskillnoskill2auto1auto2named
昂貴工作前檢查取消(§5)無無有有有
測試替身用 Mutex 同步(§4)無無有有有
參數化測試 @Test(arguments:)(§11)無無無有有
raw identifier 測試名(§15)無無無無11 個
nonisolated(unsafe)、@unchecked Sendable、.serialized無各 1無無無
auto2 的縮圖函式是最貼近 skill 第 5 節的寫法,解碼完、開始縮放之前先檢查取消:
nonisolated enum Thumbnailer {
  static let size = CGSize(width: 120, height: 120)

  @concurrent
  static func resize(_ data: Data) async throws -> UIImage {
    guard let source = UIImage(data: data) else { throw LoadError.invalidImage }
    try Task.checkCancellation()
    // 以下用 UIGraphicsImageRenderer 縮成 120×120
  }
}

named 把檢查放在別處:load() 拿到文章清單之後、寫入 state 之前,縮圖函式裡沒有檢查。兩種都算「有在意取消」,但只有 auto2 和 auto1 是照 skill 說的「昂貴工作之前」。

最能看出 skill 作用的是測試替身。noskill2 和 auto1 都選了 URLProtocol 攔截網路請求,替身需要一個全域共用的回應表,兩邊的處理方式剛好對照 skill 第 4 節的優先順序:

// noskill2:用最後手段關掉檢查,再把整個 suite 串行化
nonisolated final class StubURLProtocol: URLProtocol, @unchecked Sendable {
    nonisolated(unsafe) static var handler: Handler?
}
@Suite(.serialized)
struct NetworkLoadingTests { /* ... */ }

// auto1:用 Mutex 包住共用狀態,測試照常平行跑
private static let replies = Mutex<[URL: @Sendable () -> Reply]>([:])

skill 把 nonisolated(unsafe) 寫成「最後手段,不是用來消警告的」,@unchecked Sendable 只留給內部真的有同步機制的型別,.serialized 則是「先重構,不要拿它當解法」。noskill2 三個都用了,auto1 一個都沒用。

有一項差異不穩,我沒算進結論

@MainActor 的次數乍看也有差:noskill 在 ViewModel 上標了一次,有 skill 的三組 ViewModel 都沒標。但 noskill2 的 ViewModel 同樣沒標,只在測試 suite 上標了一次。兩次基準就分歧了,這項我不當成 skill 的效果。

行數也一樣。named 最短(311 行),noskill2 最長(559 行),但同樣沒裝 skill 的 noskill 是 462 行,樣本太少,我不下「skill 讓程式變短」的結論。

⚠️ 有哪些限制和坑?

版本基準已經落後一版

這是目前最大的坑。skill 寫在 Swift 6.4 釋出前,所以 ⚠ 那幾列會叫模型「先別用、改用舊寫法」,但你的 toolchain 如果已經是 6.4,這個保守建議就是多餘的。反過來,raw identifiers 標成 6.0,會讓 6.0、6.1 的專案寫出編不過的測試名。用之前先確認自己的 Swift 版本,再對照上面那張表。

實驗只證明寫法,不證明能跑

除了被額度打斷的 noskill,其餘四組收尾時都主動講了兩件事:這台沒有 Swift 工具鏈所以沒編譯,以及 @testable import 的模組名稱是猜的。這次對照能說明「skill 讓模型更常寫出哪些寫法」,不能說明那些程式在 Xcode 裡編得過。

Windows Git Bash 會改壞 /write-swift

在 Git Bash 裡把 /write-swift ... 當參數傳給 claude -p,MSYS 會把開頭是 / 的參數當成路徑改寫。我第一次這樣跑時,斜線命令沒有被當成點名,是模型自己呼叫 Skill 工具才載入的;連需求裡的網址也被改成 https;\\example.com\articles.json,模型收尾時還特地說它幫我改回來。前面加 MSYS_NO_PATHCONV=1 之後才是正常的點名注入。在 Claude Code 互動介面裡直接打 /write-swift 不會遇到這個問題。

Library 作者要挑著用

第 3 節「開 MainActor 預設隔離」只適用 app 和 UI 模組,同一節也寫了 library 不要開。照單全收前,先分清楚自己寫的是 app 還是要給別人用的 package。

👥 適合誰、不適合誰?

適合:

  • 讓 Claude Code 寫或改 SwiftUI app 的人。這次實測裡穩定多出來的取消檢查和測試寫法,在 code review 時都要人工補,skill 能省掉這段來回。
  • 正在把舊專案遷到 Swift 6 的團隊。第 16 節的遷移順序(先 UI 層、一個 target 一個 target、遷完再重構)可以直接當成給 agent 的工作守則。
不適合,或要搭配別的:
  • 寫網頁版的 Apple 風格互動。那是 apple-design skill 的範圍,它把 WWDC 的動效原則翻成 CSS 和 Pointer Events,不碰 Swift。
  • 讓網頁在手機上有原生 App 的手感。同 repo 的 mobile-native skill 處理的是網頁的 viewport、安全區和觸控,也不是 Swift。
  • toolchain 停在 6.1 以前的專案。skill 開頭自己寫了:第 3 節關於 async 和 @concurrent 的規則在 6.1 以前不適用。

🔗 延伸資源

下一步建議:先跑 swift --version 看自己在 6.2、6.3 還是 6.4,再用 /write-swift 請 Claude Code 審一個現有的 ViewModel,看它指出哪些 @MainActor 可以拿掉、哪裡少了取消檢查,然後在 Xcode 裡實際編譯一次。
author
陳
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。