云端 Mac 上的 iOS 列表差量更新验收

云端 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(_:) 时保留整份差量,不能见到关联就跳过其中一边。

若界面采用自己的批量更新接口,应单独实现适配器,核对它对源位置、目标位置和数据源更新时间的要求。标准库差量通过验收,只证明数组变换正确,不证明任意界面调用顺序正确。

内容刷新另走一条通道

假设 ID 为 a 的行只修改标题,ID 数组完全不变,差量就为空。你仍需比较新旧模型的可见字段,收集需要刷新的 ID。不要为了触发刷新临时生成新 ID,那会把一次内容修改变成删除加插入,并丢失选中或展开状态。

用版本检查挡住过期差量

后台开始计算时,同时捕获旧快照和单调递增的版本号。结果回到界面隔离域后,先核对当前版本仍等于捕获版本,再更新模型与界面;核对和提交之间不能插入异步等待。

如果版本已经变化,就丢弃结果,以当前快照重新计算。不要把“标准库返回非空结果”当成版本校验:重复值或碰巧兼容的位置,可能让过期差量仍能生成一个数组。

上线前最后检查四件事:重复 ID 被明确拒绝;结构变化和内容刷新分开;过期版本不会提交;界面适配器至少覆盖一次删除、移动与插入混合发生的用例。保留输入快照、版本号与失败断言即可定位问题,不必记录行内敏感内容。

常见问题

调用 inferringMoves() 后,可以跳过关联的删除和插入吗?

不可以。移动在差量中仍由关联的删除和插入表示。交给 applying(_:) 还原时要保留完整差量;转换为界面移动操作时,则需要单独适配并验证界面的索引规则。

只比较稳定 ID,能发现同一行标题改变吗?

不能。ID 差量只描述列表结构变化。标题等内容应通过独立的内容比较触发刷新,并在应用结构更新后读取最新模型。

给你的工作流接一台独享 Mac

用原生 macOS 完成构建、测试与实验。先比较配置和计费周期,再选择适合的物理节点。

立即租用云端 Mac 查看机型方案

把下一次构建交给云端 Mac

独享物理机,非虚拟机。按你的项目周期选择配置与租期。