SwiftUI中LazyVGrid滑动删除出现错误项删除问题
问题根源与修复方案
核心问题分析
- ForEach依赖索引作为唯一标识:删除操作会改变数组索引,SwiftUI的视图复用机制会错误地将旧视图绑定到新数据,导致UI显示错乱。
- 子ViewModel无唯一标识:
SnagItemViewModel未实现Identifiable,ForEach无法区分不同的列表项。 - 状态修饰符使用错误:
SnagItemRowView中用@StateObject接收父视图传入的ViewModel,会导致视图持有旧的实例,无法响应数据更新。reference用@State修饰,删除后视图复用时会保留旧的序号值,导致显示错误。
- 删除方法限制单一索引:原方法只删除第一个索引,无法正确处理批量删除场景。
修复步骤
1. 给数据模型添加唯一标识
修改SnagItem,让它遵循Identifiable协议,自动生成唯一ID:
struct SnagItem: Codable, Hashable, Identifiable { let id = UUID() private(set) var reference: Int = 0 private(set) var details: String = "" private(set) var isCompleted: Bool = false enum CodingKeys: String, CodingKey, CaseIterable { case reference = "Reference" case details = "Details" case isCompleted = "IsCompleted" } }
2. 让子ViewModel遵循Identifiable
复用SnagItem的ID作为SnagItemViewModel的唯一标识:
class SnagItemViewModel: ObservableObject, Identifiable { let id: UUID private let snagItem: SnagItem @Published var details: String = "" @Published var isCompleted: Bool = false init(withSnagItem snagItem: SnagItem) { self.id = snagItem.id self.snagItem = snagItem details = snagItem.details isCompleted = snagItem.isCompleted } }
3. 修正容器视图的ForEach实现
直接遍历Identifiable数组,通过ID计算当前项的序号:
struct SnagItemsView: View { @StateObject var snagItemsViewModel: SnagItemsViewModel var body: some View { ForEach($snagItemsViewModel.snagItems) { $snagItemVM in let reference = snagItemsViewModel.snagItems.firstIndex(where: { $0.id == snagItemVM.id })! + 1 SnagItemRowView(reference: reference, snagItemViewModel: snagItemVM) } .onDelete(perform: delete(at:)) } func delete(at indexSet: IndexSet) { withAnimation { snagItemsViewModel.snagItems.remove(atOffsets: indexSet) } } }
注:如果需要避免强制解包,可以添加默认值,或者在
SnagItemsViewModel中维护序号逻辑。
4. 修正行视图的状态修饰符
将@State改为普通常量,@StateObject改为@ObservedObject(iOS17+可替换为@Bindable):
struct SnagItemRowView: View { let reference: Int @ObservedObject var snagItemViewModel: SnagItemViewModel let gridItemColumnConfig = [ GridItem(.fixed(Constants.Frame.minGridItemWidth)), GridItem(.flexible()), GridItem(.fixed(Constants.Frame.minGridItemWidth)) ] var body: some View { LazyVGrid(columns: gridItemColumnConfig, spacing: 8) { ForEach(SnagItem.CodingKeys.allCases, id: \.self) { snagItemCategory in switch snagItemCategory { case .reference: Text("\(reference)") case .details: TextField(snagItemViewModel.localizedText(for: .enterValue), text: $snagItemViewModel.details, axis: .vertical) case .isCompleted: Toggle("", isOn: $snagItemViewModel.isCompleted) .labelsHidden() } } } } }
5. 简化删除方法
支持批量删除,直接调用数组的remove(atOffsets:):
class SnagItemsViewModel: ObservableObject { @Published var snagItems: [SnagItemViewModel] = [SnagItemViewModel(withSnagItem: SnagItem())] func addSnagItem() { snagItems.append(SnagItemViewModel(withSnagItem: SnagItem())) } func deleteSnagItemRecord(at indexSet: IndexSet) { snagItems.remove(atOffsets: indexSet) } }
修复原理
- 唯一ID让SwiftUI能精准追踪每个列表项,删除后不会因为索引变化导致视图复用错误。
- 修正状态修饰符后,行视图会正确响应ViewModel的更新,序号也会随数组结构变化重新计算。
- 直接遍历
Identifiable数组,彻底避免了索引依赖带来的UI与数据不匹配问题。
内容的提问来源于stack exchange,提问作者Shawn Frank
相关产品推荐
相关产品推荐

