雲端 Mac 上的 iOS 列表差異更新驗收
列表同時發生刪除、插入與重新排序時,最難重現的錯誤往往不是資料算錯,而是把舊位置當成新位置。你可以先在 OSXFlow 雲端 Mac 上執行純 Swift 驗收腳本,不必啟動模擬器,就能檢查差異是否能正向套用並反向還原;再將通過驗證的結構變更接到介面更新層。
先釐清差異描述的是哪兩個狀態
將更新前的穩定 ID 陣列稱為舊快照,更新後的陣列稱為新快照。呼叫方向是 new.difference(from: old),不是反過來。刪除偏移屬於舊快照,插入偏移屬於新快照,不能把兩者放在同一個不斷變動的座標系中理解。
例如,舊列表是 [a,b,c],新列表是 [c,a,d]。如果先依新位置插入,再依舊位置刪除,就可能刪掉剛插入的元素。交由標準函式庫還原時,請使用 old.applying(diff),不要自行調整每個偏移。
先訂好三項前提:同一份快照內的 ID 必須唯一;內容修改不改變 ID;背景計算差異期間,不得就地修改來源快照。ID 重複不一定會讓標準函式庫報錯,卻會讓介面中各列的身分失去明確意義。
差異不是可以套用到任何列表的修補檔。它對應的是一組確定的新舊快照;能夠成功套用,也不代表來源資料的版本正確。
用可執行腳本驗證正向套用與反向還原
在遠端終端機確認 xcrun --find swift 能找到編譯器。將下列程式碼儲存為 /tmp/list-diff-check.swift,再執行 xcrun swift /tmp/list-diff-check.swift。範例只使用標準函式庫,不需要建立專案、下載相依套件或設定簽署。
func hasUniqueIDs(_ values: [String]) -> Bool {
Set(values).count == values.count
}
func verify(_ old: [String], _ new: [String]) {
precondition(hasUniqueIDs(old))
precondition(hasUniqueIDs(new))
let raw = new.difference(from: old)
let moved = raw.inferringMoves()
precondition(old.applying(raw) == new)
precondition(old.applying(moved) == new)
precondition(new.applying(moved.inverse()) == old)
}
verify([], [])
verify([], ["a"])
verify(["a"], [])
verify(["a", "b", "c"], ["c", "a", "d"])
verify(["a", "b", "c"], ["c", "b", "a"])
precondition(!hasUniqueIDs(["a", "a"]))
執行成功時,腳本不會輸出任何內容,程序結束碼為零。任一前置條件不成立,都會終止執行,讓流水線判定為失敗。不要使用會移除這些檢查的 -Ounchecked 編譯模式。
這裡同時驗證原始差異、推斷移動後的差異,以及反向轉換。三者分別回答「能否到達目標狀態」、「建立移動關聯後是否仍保留原本語意」、「能否回到來源狀態」。只檢查元素數量無法發現位置錯亂,必須比較完整陣列。
窮舉小集合,而不只測幾個順手的案例
固定案例適合用來說明問題,但容易漏掉「刪除首項,同時將末項移到中間」之類的組合。四個不同 ID 的所有不重複有序子集共有 65 個,兩兩轉換則有 4225 組。這個規模可以直接窮舉,不需要隨機種子。
將下列程式碼附加到同一個檔案,再執行一次:
func snapshots(
prefix: [String],
remaining: [String]
) -> [[String]] {
var result = [prefix]
for index in remaining.indices {
var rest = remaining
let next = rest.remove(at: index)
result += snapshots(
prefix: prefix + [next],
remaining: rest
)
}
return result
}
let cases = snapshots(
prefix: [],
remaining: ["a", "b", "c", "d"]
)
precondition(cases.count == 65)
for old in cases {
for new in cases {
verify(old, new)
}
}
這涵蓋了空列表、純插入、純刪除、完全反轉及混合編輯,但不能證明任意長度的列表都正確。實際整合時,應將正式環境中的問題縮減為最小輸入對,再加入固定案例;大型列表則另行量測執行時間與記憶體用量,不要將這裡的執行結果視為效能保證。
移動關聯不等於介面移動指令
inferringMoves() 會為可辨識的刪除與插入建立關聯,不會將它們轉成新的列舉案例。交給 applying(_:) 時,必須保留完整差異,不能因為發現關聯就略過其中一邊。
如果介面使用自己的批次更新 API,應另外實作轉接器,確認該 API 對來源位置、目標位置及資料來源更新時機的要求。標準函式庫差異通過驗收,只能證明陣列轉換正確,不能證明任意介面呼叫順序都正確。
內容更新另走一條管道
假設 ID 為 a 的列只修改標題,ID 陣列完全不變,差異就會是空的。你仍須比較新舊模型中可見的欄位,收集需要更新的 ID。不要為了觸發更新而臨時產生新 ID,這會把一次內容修改變成刪除加插入,並導致選取或展開狀態遺失。
用版本檢查擋下過期差異
背景工作開始計算時,同時擷取舊快照與單調遞增的版本號。結果回到介面的隔離域後,先確認目前版本仍等於先前擷取的版本,再更新模型與介面;檢查與提交之間不能插入非同步等待。
如果版本已經改變,就捨棄結果,改以目前快照重新計算。不要把「標準函式庫傳回非空結果」當成版本檢查:重複值或恰好相容的位置,可能讓過期差異仍然產生一個陣列。
上線前最後檢查四件事:明確拒絕重複 ID;結構變更與內容更新分開處理;不提交過期版本;介面轉接器至少測過一次刪除、移動與插入同時發生的案例。保留輸入快照、版本號與失敗的斷言,即可定位問題,不必記錄列內的敏感內容。
常見問題
呼叫 inferringMoves() 後,可以略過關聯的刪除與插入嗎?
不可以。移動在差異中仍以關聯的刪除與插入表示。交給 applying(_:) 還原時必須保留完整差異;轉換為介面移動操作時,需要另外驗證介面的索引規則。
只比較穩定 ID,能發現同一列標題改變嗎?
不能。ID 差異只描述列表結構變化。標題等內容必須獨立比較以觸發重新整理,並在套用結構更新後讀取最新模型。
為你的工作流程接上專屬 Mac
使用原生 macOS 完成建置、測試與實驗。先比較規格與計費週期,再選擇適合的實體節點。