懶人包
- 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 或裝置上編譯執行。
- write-swift skill 是什麼、解決什麼問題?
- 16 節規則各在講什麼?
- 哪些規則對應哪個 Swift 版本?版本表準不準?
- 它什麼時候會被觸發?怎麼點名?
- 怎麼安裝到 Claude Code?
- 實測:同一個 SwiftUI 列表,有沒有 skill 差在哪?
- 有哪些限制和坑?
- 適合誰、不適合誰?
- 延伸資源
🧭 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。
📚 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 欄寫的是哪一版實作。
| skill 裡的寫法 | skill 標的版本 | swift-evolution 狀態(2026-10-21 查) | 判斷 |
|---|---|---|---|
async 留在呼叫端、@concurrent(SE-0461) | 6.2 | Implemented (Swift 6.2) | 正確 |
| Default actor isolation(SE-0466) | 6.2 | Implemented (Swift 6.2) | 正確 |
型別上的 nonisolated(SE-0449) | 6.1 | Implemented (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.0 | Implemented (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 原始碼的診斷文件裡有它,我沒確認是哪一版開始有 |
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 | 沒裝 skill | 462 | 65.6 秒 | 0.30 美元 |
| noskill2 | 沒裝 skill(重跑) | 559 | 99.9 秒 | 0.41 美元 |
| auto1 | 裝了,沒點名 | 501 | 125 秒 | 0.47 美元 |
| auto2 | 裝了,沒點名 | 399 | 108 秒 | 0.42 美元 |
| named | 裝了,開頭 /write-swift | 311 | 77.6 秒 | 0.39 美元 |
沒裝 skill 的版本已經很現代
這是我最意外的地方。兩組 noskill 都用了 @concurrent 把縮圖處理丟到背景、用 actor 加「進行中 Task 字典」去重、用 @Observable 和 Swift Testing,五組裡沒有任何一組用 Task.detached、DispatchQueue 或 XCTest。Sonnet 本身就知道 6.2 的寫法,skill 沒有從零教它。
裝了 skill 之後穩定多出來的東西
差異集中在細節,而且剛好都對得上 skill 的條文:
| 規則(skill 出處) | noskill | noskill2 | auto1 | auto2 | named |
|---|---|---|---|---|---|
| 昂貴工作前檢查取消(§5) | 無 | 無 | 有 | 有 | 有 |
測試替身用 Mutex 同步(§4) | 無 | 無 | 有 | 有 | 有 |
參數化測試 @Test(arguments:)(§11) | 無 | 無 | 無 | 有 | 有 |
| raw identifier 測試名(§15) | 無 | 無 | 無 | 無 | 11 個 |
nonisolated(unsafe)、@unchecked Sendable、.serialized | 無 | 各 1 | 無 | 無 | 無 |
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 以前不適用。
🔗 延伸資源
- GitHub repo:emilkowalski/skills
- skill 原文:skills/write-swift/SKILL.md
- Swift 6.4 發布文:swift.org/blog
- 提案狀態查詢:swift-evolution
- 同 repo 的網頁動效 skill:apple-design skill 實測
swift --version 看自己在 6.2、6.3 還是 6.4,再用 /write-swift 請 Claude Code 審一個現有的 ViewModel,看它指出哪些 @MainActor 可以拿掉、哪裡少了取消檢查,然後在 Xcode 裡實際編譯一次。