回部落格
iThome 鐵人賽Vibe CodingSwiftDataSwiftUILaravel育兒

iThome 鐵人賽 Day 11:一句話存成一筆紀錄,三條規則的解析與 SwiftData 的 migration

第一個功能完成:一句話存成一筆紀錄,三條規則解析第一次測試就判錯;整理 SwiftData migration 什麼時候不用寫、什麼時候要寫。

Steven Chang
Steven Chang
2026年9月25日 01 Min read
訂閱
iThome 鐵人賽 Day 11:一句話存成一筆紀錄,三條規則的解析與 SwiftData 的 migration

本文同步發表於 iThome 鐵人賽,系列:神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App

中秋節快樂!白天在家裡,能坐下來的時間比平常少,這篇的密度也比前幾天低。

昨天寫完架構文件,今天照規格做第一個功能:一句話進來,存成一筆事件。第一個做記錄不做同步,理由是 Day 9 排順序時寫過的,記錄這件事如果做不順,後面全部都沒有意義。

先講結果:這條路通了,build 成功,44 個單元測試全過。但解析只有三條關鍵字規則,第一次測試就判錯。我打「尿布快沒了」,它分到「紀錄」,實際上那是待辦。

1. 做出來長什麼樣

輸入畫面

首頁只有一份清單和底部的輸入框。輸入「尿布快沒了」按解析:

確認卡

卡片上有三個地方是照架構文件做的:

  • 原文保留在最上面,不管解析對不對,你講的那句話都留著
  • 候選分類老實標示推測,規則猜的是「紀錄」,我改成「待辦」,畫面上仍然寫著「候選分類:紀錄(推測)」,它不假裝自己猜對了
  • 時間不自動換算,寫「明天下午」就存「明天下午」,不會變成某月某日下午三點,下面標著「時間未解析,可自行填寫」

確認儲存之後:

存好的清單

一張卡,待辦標籤、內容、時間,下面小字寫著原文。關掉 App 再打開,還在。

2. 為什麼它會猜錯

解析的全部內容就是這三條:

規則結果
結尾是「嗎」「嗎?」「?」「?」請求
「買」或「記得」開頭待辦
其他全部紀錄(標示為推測)

沒有 AI、沒有模型、沒有時間解析。內容就是整句話,時間留空讓人自己填。

所以「尿布快沒了」歸類到第三項,因為它不是「買」開頭、結尾也沒有「嗎」。

這是 Day 9 定的「規則為主、模型為輔」的第一步:先做一個每支 iPhone 都能跑的版本,不綁裝置端模型。

代價是準確度就會不好,每記一筆都可能要停下來改一次分類。確認卡本來是為了避免存錯東西,可是如果每次都要改,記一件事反而變慢,而讓記錄變快本來是這個 App 要解決的第一件事。這個問題今天還沒解。

3. 讀 AI 寫的程式碼,要先看懂什麼

我平常寫 Laravel,SwiftData 的資料模型是這次第一次操作。對照起來大概是這樣:

LaravelSwiftData差別
Eloquent Model@Model 標記的 class欄位直接寫成屬性,不用 $fillable
migration 檔VersionedSchema + SchemaMigrationPlan不是資料夾裡的檔案,是程式碼裡宣告的版本
DB::table() 或 Model::query()@Query寫在畫面上,資料變了畫面自己更新
Eloquent 的連線設定ModelContainerApp 啟動時建立,決定資料存在哪
交易與 save()ModelContext新增、刪除、儲存都走它

這次做出來的事件模型:

@Model
nonisolated final class Event {
    @Attribute(.unique) var id: UUID
    var rawText: String
    var kindRawValue: String
    var content: String
    var timeText: String?
    var createdAt: Date
    var child: Child?   // nil is a valid answer, not a missing value
}

有三個地方跟 Laravel 的習慣不一樣。

種類存成字串,不直接存 enum。 kindRawValue 是 String,讀出來才轉回 enum。這樣之後同步從另一支手機拉到這台還不認得的種類,只會讓那一欄轉不出來,整筆資料還讀得到。

nonisolated 和 final。 Swift 6 的嚴格並行下,資料模型不能隨便跨執行緒傳遞,這兩個關鍵字是在標明它的界線。這是 Laravel 不會遇到的問題。

migration 要寫幾段,看你改什麼。 Laravel 改欄位是在資料夾裡新增一個帶時間戳的 migration 檔;SwiftData 是直接改 class,再看情況補上版本說明:

改什麼要寫幾段
加一個 optional 欄位不用寫,框架自動處理
加有預設值的欄位不用寫
改名、拆欄位、語意變了三段:舊版 schema、新版 schema、轉換的 plan

這次加「關於哪個小孩」那個欄位是 optional,所以一段都沒寫。真正要寫三段的場合不多,例如之後把 timeText 從字串改成日期才會用到。

還有一個跟 Laravel 不同的地方:Laravel 的 migration 有 up() 和 down() 可以回滾,SwiftData 只能往前,版本順序一旦發布就不能改。

這裡設立一條規則:改欄位不可以靠刪掉 App 重來。

現在比較沒關係,都是假資料。等家人手機上有兩週的紀錄,刪下去就回不來。這條特別容易犯,因為免費帳號每七天本來就要重新安裝一次,續簽跟刪掉重來在手上的動作很像。

4. 驗收看了什麼

項目結果
build成功
單元測試44 個,全過
UI 測試1 個,過
靜態掃描Persistence 以外沒有 import SwiftData、@Query、modelContext
log 檢查沒有 print、Logger,照護內容不寫進 log
網路檢查沒有 Speech、URLSession、Network 呼叫

模擬器上實際跑過:不輸入直接按解析會擋下來、取消不會存、內容改成空白會出欄位錯誤、確認之後清單出現、關掉重開還在。

有四種情況在模擬器上做不出來:

沒測到的為什麼做不出來
儲存失敗要磁碟滿了才會發生,沒辦法把模擬器的磁碟塞滿
清單讀不出來要資料庫損毀
資料庫開不起來同上
儲存中連點兩下按鈕在儲存期間已經鎖住,點不到第二下

這四種改成在單元測試裡用一個假的儲存元件,叫它固定回傳失敗,檢查畫面有沒有正確顯示錯誤、有沒有把使用者剛打的內容留著。

這張表是驗收紀錄裡它自己補上的,我沒要求。標題叫「未在模擬器上操作的項目」。

值得記下來的是這個做法:測不到的東西要講清楚為什麼測不到,而不是假裝測過。

明天

Day 12 做 M2 的交接卡:把累積的紀錄整理成一段文字,補充之後分享出去,對方不裝 App 也讀得懂。

分享

iThome 鐵人賽 Day 11:一句話存成一筆紀錄,三條規則的解析與 SwiftData 的 migration
iThome 鐵人賽 Day 11:一句話存成一筆紀錄,三條規則的解析與 SwiftData 的 migration

第一個功能完成:一句話存成一筆紀錄,三條規則解析第一次測試就判錯;整理 SwiftData migration 什麼時候不用寫、什麼時候要寫。

Gooliya 鼓櫟數位
Steven Chang

Steven Chang

軟體工程師,平常主要有個人接案、DevOps、系統開發、維運。研究 AI 是為了省下時間陪小朋友。

專案卡住了,或是想把重複的工作交出去?

我做 AI 專案救援、系統架構開發、AI Agent 與流程自動化。先聊聊你的情況,不用先想清楚要做什麼。

免費訂閱 Gooliya 鼓櫟數位電子報

雜談 AI、開發與自動化,新文章上線時寄給你。

訂閱服務由 Kit 提供,Email 只用來寄送新文章