iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Vibe Coding

神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 系列 第 30 篇

[Day 30] 30 天回顧:最後的架構,以及 AI 和 Vibe Coding 怎麼協作

  • 分享至 

  • xImage
  •  

Day 30

文章同步發表在我的個人 Blog

30 天回顧:最後的架構,以及 AI 和 Vibe Coding 怎麼協作

30 天做完,留下來的是什麼?

昨天 Day 29 整理 AI 常寫錯的地方。今天是最後一天,回頭看四件事:投票最後的結果、這 30 天做出了什麼、最後的架構怎麼讓 AI 分工,以及哪些東西可以繼續用。

先講結果:投票頁最後是 19 個人、52 個圓點,最後一票在 10/1,前兩名從第一週到最後都沒變。Day 21 寫的 23 個人、64 個圓點多算了 4 筆測試資料,這裡更正。這個 App 的功能細節,我不在系列文章裡公開,所以對照的部分只寫到 Day 13 已經寫過的內容。架構上,領域規則全部放在不依賴 UI 和 Firebase 的 TeammateKit,Firebase 與 Google 登入只出現在 Teammate/Persistence/,這條邊界由 verify.sh 的掃描來檢查。也是因為這樣切,Claude Code、Codex 和我才分得了工。留下來可以繼續用的,是規則文件、一行就能跑完的驗證腳本,以及每一輪的記錄。

1. 投票最後的結果

投票頁用同一個查詢,排除 9/19 凌晨的 4 筆測試資料之後,結果如下:

選項 選的人數 圓點數
做了好多事,另一半不知道,會覺得分工不均 12 15
今天吃什麼、要煮什麼,永遠是臨時才有人想 13 14
想休息,但沒先講,等到快爆炸才說 7 9
週末排了什麼課程,忘記紀錄、提醒,到當天才發現 7 8
出門前才發現東西沒帶,因為沒講好誰要準備 4 4
訪客補充:爸爸可以不要看手機嗎? 2 2
訪客補充:爸爸廁所要待多久? 0 0

人數是「有幾個人選了這一項」,每個人最多貼 3 個圓點,同一個選項可以貼不只一個,所以圓點數會比人數多。五個原本的選項加起來 50 個圓點,加上訪客補充的 2 個,剛好是 52。

前兩名從第一週就是同樣兩個選項。依選的人數排,「今天吃什麼」第一;依圓點數排,「做了好多事,另一半不知道」第一,兩種排法的前兩名一樣。

9/19 到 9/23 這五天有 15 個人、43 個圓點,之後 9/28 到 10/1 只多了 4 個人、9 個圓點,最後一票在 10/1。19 個人不算多,這份結果我當成方向的參考,不當成統計結論。

更正 Day 21:我寫的「23 個人、64 個圓點」是包含測試資料的總數,Day 13 已經排除的 4 筆測試也算進去了。扣掉之後是 19 個人、52 個圓點,Day 21 表格裡每個選項的數字也都多了測試的部分,以這一篇的表格為準。

2. 前兩名,對照做出來的東西

Day 13 的結論是:前兩名都需要讓另一半知道,一個人用的 App 做不到。所以重做的版本有三個核心:每一件事記錄對方知道了沒有、兩支手機用 Firebase 同步、用本機通知提醒。這三件事是 Day 14 到 Day 25 的主線,從重做、Firebase 正式專案、Security Rules,到匿名帳號升級成 Google 登入。

這之外的功能,包含排名第一的「今天吃什麼」,以及其他幾個選項怎麼處理,我不在這裡寫。投票頁的用途是決定第一版的方向,這件事在 Day 13 已經用到了。

3. 30 天走過的路

Day 做了什麼
1~6 收集痛點、做投票頁
7~12 方法與規範、安裝 Xcode、第一版
13 投票第一週結果,第一版重做
14~20 重做的版本、Firebase、裝到實機、Security Rules、把缺口寫成檢查
21 中場回顧
22~24 大字級版面、不用兩支手機測同步、對規則做變異測試
25~26 Google 登入、簽章到期後重新安裝
27~28 英文語系、收集回饋與設定頁語言選項
29 Vibe Coding 總檢討

4. 依賴只能往內,Firebase 只留一個出口

程式分成五塊,箭頭是依賴的方向:

最後的架構

Day 25 加進來的 GoogleSignIn 套件,整個專案只有 FirebaseAccountLinker.swift 一個檔案 import 它,畫面層和 TeammateKit 都只認 AccountLinking。ADR-005 的做法是用 linkWithCredential 把 Google 帳號綁到既有的匿名帳號,uid 不變,所以已經建立的家庭和安全規則都不用動。如果 Google 登入不能用,退路是 Email 連結登入,只需要重寫 Persistence 裡的實作。

資料結構只有五個集合:

households/{hid}                  家庭
households/{hid}/members/{uid}    成員(文件 id 是匿名 uid)
households/{hid}/cards/{cardId}   卡片
invites/{code}                    邀請碼(文件 id 是 6 位數字)
userHouseholds/{uid}              這個帳號屬於哪個家庭

規則檔只開放這五條路徑,其他一律拒絕,新功能要先在規則加白名單。households、invites、userHouseholds 完全不能查詢,members 和 cards 只有該家庭的成員能查詢,所以 App 永遠是用已知的 id 直接取文件。createdAt 這類時間欄位必須是 FieldValue.serverTimestamp(),規則要求它等於 request.time,手機自己填的時間會被拒絕。

5. 規則寫成純函式,時間從外面傳進來

規則需要「現在」的地方,一律吃 Clock protocol 或 now 參數,不自己讀系統時間:

public protocol Clock: Sendable { var now: Date { get } }

// Rules never read the system clock
AcknowledgementRules.acknowledge(card, by: viewer, clock: clock)
NotificationPlanner.plan(events: events, cards: cards, viewer: viewer,
                         members: members, settings: settings, now: now)

測試用固定時鐘,要驗證「明天晚上八點」或「24 小時後過期」,不用等,也不用改系統時間。

「對方知道了沒有」也是一組純函式。卡片有 ackBy,nil 代表對方還沒知道。AcknowledgementRules.pending 只回傳「ackBy 是 nil,而且最後編輯的人不是自己」的卡片;最後編輯的人想確認自己的卡,會丟出 lastEditorCannotAcknowledge。編輯時,只要標題、時間或負責人變了,ackBy 就清回 nil,對方要重新知道一次,只改備註不會。App 圖示上的數字,也是同一個 pending 算出來的筆數。這些規則在 TeammateKit 裡用 swift test 幾秒內跑完,不需要模擬器,也不需要連 Firebase。

通知的重排只動真的有變的。沒有遠端推播(免費 Apple 帳號用不了,ADR-003),所以 App 內的「待你知道」是唯一保證的管道,通知是盡力而為的加分。資料同步之後,NotificationPlanner.plan 算出「現在應該排哪些本地通知」,diff 和已經排好的比,reschedule 只套用差異:

// Same id: overwrite in place. Cancel only ids that are really gone.
let replacedIDs = Set(changes.toSchedule.map(\.id))
let removedIDs = changes.toCancel.filter { !replacedIDs.contains($0) }
await scheduler.cancel(ids: removedIDs)
try await scheduler.schedule(changes.toSchedule)

內容或時間變了的通知,用同一個 id 直接覆蓋,不先 cancel 再 schedule。原因寫在程式註解裡:removePendingNotificationRequests 在背景非同步執行,可能在新增之後才刪,把剛排好的通知刪掉。

背景刷新是另一層。BGAppRefreshTask 只能「提出請求」,程式裡提的是最早 15 分鐘後,什麼時候真的執行由 iOS 決定,所以 ADR-003 把它列為可靠度最低的一層。

6. 用記憶體裡的雲端,測兩支手機同步

InMemoryBackend 是一個 actor,多個 InMemoryHouseholdService、InMemoryCardRepository 共用同一個 backend,就能在單元測試裡模擬兩支手機互相同步,不需要 Firebase。它實作的規則和 Firestore 安全規則一致:只有成員能讀寫、邀請碼 24 小時一次性、家庭最多兩人。每個 ViewModel 都能用這組 in-memory 實作建立,所以畫面邏輯的測試,也不用等 Emulator 啟動。

7. Security Rules 擋了哪些事

規則檔預設全部拒絕,子集合用白名單。判斷成員資格用 exists(),不用 existsAfter(),避免有人在同一批寫入裡先把自己加進成員、再順便讀寫。用邀請碼加入家庭時,「消耗邀請、建立自己的成員、把自己加進家庭」三份文件用 getAfter() 互相綁定,少任何一份整批失敗。邀請碼 24 小時有效,用伺服器的 request.time 計算,不信任手機時間。

規則檔裡也寫了已知的限制:6 位數的邀請碼可以被逐一猜測,規則沒辦法限制嘗試次數,所以用「24 小時、只能用一次、家庭滿兩人就拒絕」縮小影響範圍。這份規則有 99 個測試,在 Firebase Emulator 上跑。

8. verify.sh 怎麼檢查這些規則

./scripts/verify.sh 分五步:產生專案、TeammateKit 單元測試、Emulator 上的安全規則與 App 測試、掃描自我測試、靜態掃描。靜態掃描 scripts/scans.sh 有七項:

掃描 檢查什麼
禁用詞 不得有計分、排行、次數統計
硬編中文 UI 文案都在 Localizable.xcstrings
寫死樣式 Features/ 不寫顏色、字級、間距數值
付費 capability 免費帳號不能用的功能不得加入
元件 #Preview 每個元件都有預覽
Firebase 邊界 import Firebase* 只能出現在 Teammate/Persistence/
App 圖示 每個尺寸都有檔案,PNG 尺寸等於宣告的尺寸

最後一項的來源是實機:App 圖示只有 Contents.json、沒有圖片,建置不會報錯,到手機上才看到空白圖示,所以寫成檢查。

掃描本身也要被測試。scripts/scans-selftest.sh 用假檔案確認每一項真的抓得到,例如在 Teammate/Features/Home/HomeView.swift 放一行 import FirebaseFirestore,firebase-boundary 一定要失敗;在 TeammateKit 的 CardRepository.swift 放同一行,也一樣。沒有這一步,掃描壞掉變成永遠不命中,最後一行還是 VERIFY: PASS。

9. 這個架構怎麼讓 Claude Code、Codex 和我分工

分工寫在 AGENTS.md 第 8 節,Claude Code 和 Codex 共用同一份:

角色 誰 做什麼
主 Builder Claude Code 依 BACKLOG.md 實作標 [cc] 的項目、寫測試、跑 verify.sh、整合
副 Builder Codex 標 [codex] 的項目:邊界清楚,只動 TeammateKit 的純函式或單一 Feature 資料夾,在獨立 worktree 的 codex/<項目編號> 分支上做
Reviewer Codex 每個里程碑唯讀 review,結果寫進 reviews/
Adversary Codex 同步、分享、通知這類高風險設計,用 adversarial review 挑戰假設
決定與動手 我 方向與取捨,例如 ADR-005 採用 Google 登入;AI 做不到的事,例如 Firebase Console、實機安裝、第二支手機

交叉規則是「誰寫的程式碼,就由另一方 review」。Codex 寫的項目合併前,由 Claude Code 對照驗收條件讀 diff;Claude Code 寫的,由 Codex 審。

這個分工要成立,靠的是前面幾節的架構。Codex 實作的 4 個項目都是不依賴畫面的純邏輯。TeammateKit 不依賴 UIKit、SwiftUI 和 Firebase,所以一個 worktree 只動一個資料夾,不會和 Claude Code 同時改同一個檔案。

兩個 Agent 也都不連雲端專案。所有測試連 Firebase Emulator,TeammateKit 的測試用 InMemoryBackend。ADR-002 在風險表裡寫了這個考量:Firebase 的 SPM 套件編譯慢,會拖慢 Agent 的迴圈,所以領域邏輯全放在 TeammateKit,swift test 維持在幾秒內。

兩邊用同一個判官,就是 ./scripts/verify.sh 最後一行的 VERIFY: PASS 或 VERIFY: FAIL。CLAUDE.md 規定每一輪結束前,要把這一行和 BACKLOG 的勾選狀態印在對話裡,因為評估的一方只看得到對話內容。

AGENTS.md 第 2 節有九條禁止事項,不靠 Agent 自己記得。第 1 條不計分、第 8 條不寫死樣式、第 9 條不加付費 capability,都有對應的掃描;Firebase 只能出現在 Persistence,是第 3、4 節的架構規則,也有掃描。其他像「不得刪除或弱化既有測試」,靠的是 review 和 PROGRESS.md 的記錄。

需要人動手的事,規則是寫進 docs/QUESTIONS.md 的「需要 Steven 動手」區,然後跳過,不自己假設。Day 29 整理的 26 條踩坑,來源就是這個流程裡 PROGRESS.md 的記錄。

10. 30 天留下來的數字

最後一次 ./scripts/verify.sh(10/13)的結果是 VERIFY: PASS,各項數字如下:

項目 數字
commit 61 個(9/27 到 10/13,新專案)
TeammateKit 單元測試 59 個
安全規則測試 99 個
App 測試(XCTest 與 XCUITest) 223 個
掃描自我測試 50 個案例
架構決策紀錄(ADR) 4 份

11. 留下來可以繼續用的東西

下一輪要開始,用到的是這幾樣:

  • AGENTS.md:Claude Code 和 Codex 共用的規則,包含禁止事項與每一輪的流程。
  • ./scripts/verify.sh:一行指令跑完單元測試、安全規則測試、App 測試和掃描,最後一行印出 VERIFY: PASS 或 VERIFY: FAIL。
  • PROGRESS.md:每一輪做了什麼、踩到的坑、下一步。
  • Spectra 的 specs 與 changes:每個功能的規格和任務,做完 archive 回主規格。
  • docs/decisions/:架構決策紀錄,說明為什麼沒有推播、為什麼用 Firebase。

新的一輪從 PROGRESS.md 的「下一步」開始,照 Day 29 的七個步驟做,結束前印出 VERIFY: PASS。Day 28 的回饋流程也是這樣走的:收到回饋、分類、挑一個修、驗證、記錄。

如果你要開始自己的 Vibe Coding 專案,我建議第一天只做三件事:把驗收條件寫成有編號的條目、寫好一行就能跑完的驗證腳本、把 AI 不能做的事寫進規則文件。其他的,在做的過程中再補。

12. 我的心得

終於完成 30 天的鐵人賽了。

做完之後,我的感覺是 coding 的門檻大幅降低,能力的重心往創意、整合和規劃移動。但領域知識還是很重要,有了它,可以少繞很多冤枉路。

舉幾個這 30 天做下來有用的做法,給想用 Vibe Coding 做專案的人參考:

  • 一輪只做一個項目,結束前把做了什麼、卡在哪、下一步寫進 PROGRESS.md,下一輪從這份記錄開始。
  • 先寫測試再實作,測試名稱帶驗收條件編號,例如 test_AC_TOPIC_03_replyByOverdueNotifiesBoth,之後才知道每一條條件有沒有被測到。
  • 看到 VERIFY: PASS 之前,先看有沒有真的跑到測試。新增測試檔沒重跑 xcodegen generate,報告會是「執行 0 個、沒有錯誤」。
  • 同一個錯誤連續兩輪修不好,就寫進 PROGRESS.md 的卡點,改做下一項,不要一直在同一個地方打轉。

參考資源

系列文章


上一篇
[Day 29] Vibe Coding 總檢討:PROGRESS.md 記下的 26 條踩坑分五類,以及兩個 Agent 的分工結論
系列文
神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言