
文章同步發表在我的個人 Blog
SwiftUI 的 confirmationDialog 一行程式都沒改,到了 iOS 26,出現的位置卻變了。
Day 15 預告今天要裝到兩支 iPhone。裝機前,先把模擬器上用起來不順的地方調整好:卡片可以編輯和刪除,首頁多了一區「之後」,7 天以後的事也看得到。裝機改到 Day 18。

先講結果:兩份規格共 14 項任務全部完成,App 測試從 122 個增加到 142 個,verify.sh 通過。
今天的技術主軸在 Day 7 學習地圖的第二層 SwiftUI:iOS 26 的確認對話框換了呈現方式,畫面和 UI 測試都要跟著調整。
Day 7 定下「Spectra 寫規格、Claude Code 實作」的分工,Day 8 在第一版的 repo 初始化過 Spectra;這次在 v2 的 repo 也建立起來:
spectra init --tools claude # openspec/ + Claude Code skills
spectra new change <change-name> # one folder per change
spectra analyze <change-name> # consistency check
spectra validate <change-name> # format check
一個 change 由四份文件組成,照順序寫:
| 文件 | 寫什麼 | 不寫什麼 |
|---|---|---|
| proposal | 為什麼要改、改哪些、不做哪些 | 實作細節 |
| spec | 每一條需求和情境(WHEN/THEN) | 怎麼寫程式 |
| design | 技術上怎麼做、為什麼選這個做法 | 行號、逐行步驟 |
| tasks | 可以打勾的任務,每項寫明怎麼驗證 | 「修改某檔案」這種沒有驗收的任務 |
每一條情境都給一個驗收編號,之後測試名稱直接帶這個編號,格式是 test_AC_<功能>_<編號>_<情境>。規格裡的情境和測試一對一,AI 做完有沒有漏,對編號就知道。
寫規格的過程中,Claude Code 會把需求沒講清楚的地方整理成選擇題問我。今天我總共回答了七題,都是「誰可以做」「做了之後另一半那邊會發生什麼事」這一類;這些 AI 不能自己假設,專案規則寫明有疑義就要停下來問。
規格寫到一半,出現了一個問題。
Claude Code 一開始跟我說,從首頁「+」丟的事和從行事曆建立的行程是兩種資料,所以權限可以分開設定;我照這個說法先回答了權限相關的題目。等到要寫「對方可不可以改」的情境時,它得把每一條規則對到實際的資料欄位和安全規則,才發現兩個入口建立的其實是同一種資料,存在同一個地方,程式碼裡沒辦法分辨是從哪裡建立的。
前提錯了,前面的答案也就不成立。所以 Claude Code 把更正寫進問題紀錄,重新問我一次,這次我選了跟現有行為一致的做法:安全規則不需要修改,既有的測試也不用動。
照錯的前提寫出來的第一版規格裡,排了修改安全規則、改寫一條既有測試的任務;如果跳過規格直接實作,這些改動會直接進到程式碼。規格這一步的用處,就是讓 AI 在動手前先把需求對到程式碼,錯的前提在文件階段就會被發現。
第一份規格的四份文件寫完,spectra validate 格式檢查通過;接著跑 spectra analyze,出現 8 個 Warning:
Requirement '…' has no matching task
Design topic '…' not referenced in tasks
前 4 個是 spec 裡的需求,在 tasks 裡找不到對應的任務;後 4 個是 design 裡的技術決定,tasks 沒有提到。任務其實都有寫,只是沒有標出它在實作哪一條需求。
於是在每一項任務後面補上對應的需求與設計名稱:
- [ ] 1.2 ...(Requirement: <name>;Design: <topic>)
再跑一次 spectra analyze,Warning 歸零。這個對照讓實作的 AI 看到任務時,知道要回頭讀哪一段規格。
刪除一筆資料前要跳出確認,SwiftUI 用 confirmationDialog。iOS 18 以前,它在 iPhone 上是從畫面底部升起的 action sheet;iOS 26 改成從觸發它的元件長出來。Apple 的 Liquid Glass 採用指南寫得很清楚:
An action sheet originates from the element that initiates the action, instead of from the bottom edge of the display.
同一份指南也提醒要指定來源:
Specify the source of an action sheet. Position an action sheet's anchor next to the control it originates from.
在 SwiftUI 裡,「來源」就是 .confirmationDialog 掛在哪一個 view 上。Claude Code 第一次把它掛在整個詳情頁的內容上,對話框就從畫面中間長出來,箭頭指向內容正中央。改掛到工具列的「更多」按鈕之後,對話框就從按鈕旁邊出現:
Menu { /* edit, delete */ } label: { Image(systemName: "ellipsis") }
.confirmationDialog("delete.title", isPresented: $isConfirming, titleVisibility: .visible) {
Button("delete.confirm", role: .destructive, action: delete)
Button("delete.keep", role: .cancel) {}
}

另外一個差異:在模擬器上實際看到的 popover 樣式沒有顯示取消按鈕,點對話框外面就是取消。
呈現方式一變,原本的 UI 測試寫法就找不到按鈕:
tap() 會因為找到多個符合的元素(Multiple matching elements)而失敗。Claude Code 的改法:
// popover nests the button twice with the same identifier
app.buttons.matching(identifier: "deleteConfirm").firstMatch.tap()
// cancel = tap outside the popover
app.otherElements["PopoverDismissRegion"].firstMatch.tap()
PopoverDismissRegion 是 popover 外圍那塊區域在元素樹裡的名稱,UI 測試點它,效果跟使用者點外面一樣。
這次照規格要求先寫測試、確認紅燈,再寫實作。Claude Code 回報了兩個實際遇到的狀況。
第一個:同一個 test target 裡,只要有一個測試檔編譯不過,整個 target 的測試都跑不起來。Swift 的測試 target 是一整個 module 一起編譯,所以「先寫測試」在 Swift 裡的紅燈,常常是編譯錯誤,不是測試失敗。兩個 change 的測試放在同一個 target,只好先把其中一個的實作補齊,另一個的 UI 測試才跑得起來。
第二個:刪除成功後,畫面會閃一下「找不到這件事」。原因是 Firestore 的即時監聽先把資料從串流移除,詳情頁還沒關,畫面就先重畫成找不到。解法是 ViewModel 在刪除前留一份快照,關閉前都顯示快照:
var displayedCard: Card? { card ?? cardBeingDeleted }
| 項目 | 結果 |
|---|---|
| 規格 | 2 份,共 14 項任務,全部完成 |
| TeammateKit 測試 | 48 個 |
| 安全規則測試 | 79 個,沒有修改 |
| App 測試 | 142 個(新增 20 個) |
verify.sh |
VERIFY: PASS |
| 截圖審查 | Light、Dark、大字級都截圖自審,沒有 P0、P1 |
還沒做的部分:
Day 17 看 Apple 官方的 iPhone Duo 影片與工作坊,整理官方資源、設計原意,評估神隊友要怎麼適配。
官方文件