SwiftUI中如何为List行变更保留默认交换动画?
SwiftUI List行交换动画失效问题说明
我想搞懂动画底层原理,所以自己实现SwiftUI List行的变更动画,第一种写法已经实现了预期效果:
struct First: View { @State var items = ["One", "Two", "Three", "Four"] var body: some View { VStack { List(items, id: \.self) { item in Text(item) } Button("Hit me") { withAnimation { items.swapAt(2, 0) } } } } }
点击按钮后,单元格会有流畅的交换动画,能看到单元格实际上下移动:

但改成用索引遍历的写法后,交换动画就没了,变成了文本变更的动画:
List(items.indices, id: \.self) { index in Text(items[index]) }

请问有没有办法保留第一种默认的交换动画?
解决方案
核心原因是两种写法的id绑定逻辑完全不同:
- 第一种写法里,List的
id绑定的是数组元素本身,交换元素位置时,每个单元格对应的id跟着元素移动,SwiftUI会识别出是单元格位置变化,从而触发位置交换动画。 - 第二种写法里,List的
id绑定的是数组索引,交换元素后,每个索引对应的位置没变,只是索引指向的文本内容变了,SwiftUI会认为是单元格内容更新,所以只触发文本变更动画。
要保留交换动画,你有两种可行方案:
方案1:给元素绑定稳定标识(推荐)
如果你的数据是自定义类型,给每个元素添加唯一且不变的id属性;如果是字符串这类基础类型,直接用元素本身作为id(也就是回到第一种写法)。比如自定义模型的写法:
struct Item: Identifiable { let id = UUID() var name: String } struct First: View { @State var items = [Item(name: "One"), Item(name: "Two"), Item(name: "Three"), Item(name: "Four")] var body: some View { VStack { List(items) { item in Text(item.name) } Button("Hit me") { withAnimation { items.swapAt(2, 0) } } } } }
这种方式下,元素的id不随位置变化,SwiftUI能准确识别元素的位置变更,自然会触发交换动画。
方案2:手动维护索引与元素的绑定(不推荐,仅作思路参考)
如果必须用索引遍历,你需要让SwiftUI知道每个索引对应的“元素身份”,可以通过给单元格添加matchedGeometryEffect来关联元素的位置变化,但这种写法复杂且遇到重复元素会失效:
struct First: View { @State var items = ["One", "Two", "Three", "Four"] @Namespace private var animationNamespace var body: some View { VStack { List(items.indices, id: \.self) { index in Text(items[index]) .matchedGeometryEffect(id: items[index], in: animationNamespace) } Button("Hit me") { withAnimation { items.swapAt(2, 0) } } } } }
内容的提问来源于stack exchange,提问作者OhadM
相关产品推荐
相关产品推荐

