qwertyyb/Fire

候选词顺序乱了

Closed this issue · 1 comments

image
出现如上图所示的问题

期望结果:
image

分析情况:

候选词的位置,也就是顺序是正常的
index 前缀异常
当前选择的候选词不正确

背景

候选词的渲染是通过 ForEach 做的,在使用中,ForEach 要求输入 id 作为唯一标识,当时不求甚解使用 \.self

排查问题

由于上述背景原因,而且并未对出现的bug进行深入的分析,所以一开始认为是 ForEach 的参数 id 使用 \.self 并不对保证唯一导致的。

尝试解决:

经过一翻搜索查询,发现:
对于符合 Hashable 的类型,\.self 表示此对象/结构体的 hashValue 字段
对于实现了 Identifiable 的数据类型, \.self 表示对象/结构体的 id 字段

而候选词的数据类型仅仅是 Hashable 并不 Identifiable
image

所以一开始解决方案是把候选词类型改为 Identifiable 并声明定义 id = UUID() 以保证生成唯一的标识
image

运行测试发现bug被修复
到此,bug被解决,故事本该结束

再次思考:

同样的资料发现,原候选词的 Hashable 类型应该是能正常工作的,也就是能作为唯一标识。原因是结构体的默认hash一致性实现是把结构体中可hash的值一起hash运算得到的,所以不可能会有两个不一样的候选词而hash一致的问题。

为了验证上面的结论,输出候选词的 hashValue 发现,hashValue 确实不一样的
image

但是同时发现了另一个问题,候选词的执行顺序和 ForEach 的第一个参数竟然不一致,这是什么情况??

经过思考和实验,以及在各种文档的协助下,总算弄明白了 ForEach 的工作模式

  1. ForEach 中的 View 为了能够实现流畅动画效果,使用 id 参数来确定集合中元素的唯一性
  2. 在执行 ForEachcontent 之前,会对原始输入的集合进行处理,这种处理分两个方面
    a. 如果集合中有多个元素具有相同的唯一标识,那么 content 回调只会执行一次。例如,如果页面中有两个相同的 String hello 那么 content 只会被调用一次,第二个hello会直接渲染第一个的调用结果
    b. 集合会被重新排序后再依次调用 content,然后再返回值按原顺序进行渲染,我想之所以有这样的结果,应该是出于动画的渲染考虑。

再回来分析这个BUG,发现其实也就只有 2.a 这么一个解释。
至于具体为什么候选词 "去、云、支...." 会按照 "支、去、云..." 这样的顺序来渲染?
分析后,猜想是因为在前一个输入的候选词中也出现了 "支" 这个候选词,并且这两个候选词的数据是完全一致的,生成了一样的 hashValue, 所以 ForEach 在实际执行时,把前面刚出现的数据放在了前面执行。
至于为什么方法1也能修复这个问题,原因也在这里。在使用UUID作为唯一标识的情况下, 即使两个相同的候选词,ForEach 中也认为是不一样的,所以自然不会有因为前面出现过某个数据而把这个数据提前的优待,所以重排后的顺序就会仍然和输入时一致。

最终解决方案

再回来解决这个问题,主要就是因为生成的 index 是一个外部变量,在 content 回调中执行 index += 1 的操作
image

所以在 ForEachcontent 并不按照输入顺序调用的情况下,出现了index的错乱。

明白了原因,就会发现方法1并不是最完美的解决方案——如果我们后面要在ForEach里面做动画,采用方法1就可能会导致不可预期的结果,在没搞明白 ForEach 工作原理的情况下,方法1等于是埋了一个坑。

那我们如何来完美的解决这个问题呢?其实只需要候选词的index不受 ForEach 的影响就行了,比如说直接放在候选词数据中是一个方案,更好的方案在于,swift数组已经为我们提供了这样的一个方案——enumerated方法,此方法会生成一串包含 (index, element)的元组,这样最终的解决方案如下:
image