
文章同步發表在我的個人 Blog
中秋節快樂!白天在家裡,能坐下來的時間比平常少,這篇的密度也比前幾天低。
昨天寫完架構文件,今天照規格做第一個功能:一句話進來,存成一筆事件。第一個做記錄不做同步,理由是 Day 9 排順序時寫過的,記錄這件事如果做不順,後面全部都沒有意義。
先講結果:這條路通了,build 成功,44 個單元測試全過。但解析只有三條關鍵字規則,第一次測試就判錯。我打「尿布快沒了」,它分到「紀錄」,實際上那是待辦。

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

卡片上有三個地方是照架構文件做的:
確認儲存之後:

一張卡,待辦標籤、內容、時間,下面小字寫著原文。關掉 App 再打開,還在。
解析的全部內容就是這三條:
| 規則 | 結果 |
|---|---|
| 結尾是「嗎」「嗎?」「?」「?」 | 請求 |
| 「買」或「記得」開頭 | 待辦 |
| 其他全部 | 紀錄(標示為推測) |
沒有 AI、沒有模型、沒有時間解析。內容就是整句話,時間留空讓人自己填。
所以「尿布快沒了」歸類到第三項,因為它不是「買」開頭、結尾也沒有「嗎」。
這是 Day 9 定的「規則為主、模型為輔」的第一步:先做一個每支 iPhone 都能跑的版本,不綁裝置端模型。
代價是準確度就會不好,每記一筆都可能要停下來改一次分類。確認卡本來是為了避免存錯東西,可是如果每次都要改,記一件事反而變慢,而讓記錄變快本來是這個 App 要解決的第一件事。這個問題今天還沒解。
我平常寫 Laravel,SwiftData 的資料模型是這次第一次操作。對照起來大概是這樣:
| Laravel | SwiftData | 差別 |
|---|---|---|
| Eloquent Model | @Model 標記的 class |
欄位直接寫成屬性,不用 $fillable |
| migration 檔 | VersionedSchema + SchemaMigrationPlan |
不是資料夾裡的檔案,是程式碼裡宣告的版本 |
DB::table() 或 Model::query() |
@Query |
寫在畫面上,資料變了畫面自己更新 |
| Eloquent 的連線設定 | ModelContainer |
App 啟動時建立,決定資料存在哪 |
交易與 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 重來。
現在比較沒關係,都是假資料。等家人手機上有兩週的紀錄,刪下去就回不來。這條特別容易犯,因為免費帳號每七天本來就要重新安裝一次,續簽跟刪掉重來在手上的動作很像。
| 項目 | 結果 |
|---|---|
| build | 成功 |
| 單元測試 | 44 個,全過 |
| UI 測試 | 1 個,過 |
| 靜態掃描 | Persistence 以外沒有 import SwiftData、@Query、modelContext |
| log 檢查 | 沒有 print、Logger,照護內容不寫進 log |
| 網路檢查 | 沒有 Speech、URLSession、Network 呼叫 |
模擬器上實際跑過:不輸入直接按解析會擋下來、取消不會存、內容改成空白會出欄位錯誤、確認之後清單出現、關掉重開還在。
有四種情況在模擬器上做不出來:
| 沒測到的 | 為什麼做不出來 |
|---|---|
| 儲存失敗 | 要磁碟滿了才會發生,沒辦法把模擬器的磁碟塞滿 |
| 清單讀不出來 | 要資料庫損毀 |
| 資料庫開不起來 | 同上 |
| 儲存中連點兩下 | 按鈕在儲存期間已經鎖住,點不到第二下 |
這四種改成在單元測試裡用一個假的儲存元件,叫它固定回傳失敗,檢查畫面有沒有正確顯示錯誤、有沒有把使用者剛打的內容留著。
這張表是驗收紀錄裡它自己補上的,我沒要求。標題叫「未在模擬器上操作的項目」。
值得記下來的是這個做法:測不到的東西要講清楚為什麼測不到,而不是假裝測過。
Day 12 做 M2 的交接卡:把累積的紀錄整理成一段文字,補充之後分享出去,對方不裝 App 也讀得懂。