SwiftUI双层Lazy Stack用ForLoop时触发「数组越界致命错误」
SwiftUI嵌套数组删除行时偶发索引越界崩溃问题
我维护的SwiftUI界面中,存在嵌套数组结构(AssetsRowModel数组包含AssetItem数组),删除某一行时偶尔会触发崩溃,错误提示:
'Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range'
且无法定位到具体代码行。
最小复现代码
// View components struct ContentView: View { @StateObject var viewModel: ViewModel = .init() var body: some View { ScrollView { LazyVStack { ForEach($viewModel.assetsRows, id: \.self) { assetsRow in VStack { Button(action: { viewModel.deleteSelected(assetsIn: assetsRow.wrappedValue) }, label: { HStack { Image(systemName: "trash") Text("Delete row") } }) RowView(assetsRow: assetsRow) } } } } } } struct RowView: View { @Binding var assetsRow: AssetsRowModel var body: some View { ScrollView(.horizontal) { LazyHStack { ForEach($assetsRow.items, id: \.self) { item in GridItemView( assetItem: item, image: .init(systemName: "photo.fill") ) } } } } } struct GridItemView: View { @Binding var assetItem: AssetItem @State var image: Image? var body: some View { Group { if let image = image { image } else { ProgressView() } } .frame(width: 200, height: 120) .overlay(alignment: .bottomTrailing) { Toggle(isOn: $assetItem.isSelected) { Text("checkmark") } .padding(4) } .onAppear { // fetch image logic } } } @MainActor final class ViewModel: ObservableObject { @Published var assetsRows: [AssetsRowModel] = { var array: [AssetsRowModel] = [] for i in 0..<30 { array.append(.init(items: [.init(), .init(), .init()])) } return array }() // removing items causes crash (not 100% times) func deleteSelected(assetsIn row: AssetsRowModel) { withAnimation { assetsRows.removeAll { element in element.id == row.id } } } // other fetching logic } // Models struct AssetsRowModel: Identifiable, Equatable, Hashable { var id = UUID() var items: [AssetItem] } struct AssetItem: Identifiable, Hashable { var id = UUID() var isSelected = false } extension AssetItem: Equatable { static func ==(lhs: AssetItem, rhs: AssetItem) -> Bool { (lhs.id == rhs.id) } }
已尝试的无效方案
将RowView中的@Binding var assetsRow: AssetsRowModel改为@State,虽能避免崩溃,但isSelected无法与ViewModel中的值同步,不符合业务需求。
问题分析与修复方案
问题根源
LazyVStack/LazyHStack结合ForEach($array, id: \.self)使用时,SwiftUI的懒加载机制可能在数组元素删除后,仍持有已失效的绑定引用,尤其是在动画过程中,内部视图尝试访问已被移除的数组索引,触发越界错误。
修复步骤
改用
Identifiable原生重载的ForEach
由于AssetsRowModel已实现Identifiable,直接使用ForEach($viewModel.assetsRows)即可,无需指定id: \.self,让SwiftUI通过唯一id精准跟踪视图身份。优化删除方法的参数传递
直接传递行的id而非整个实例,避免引用失效问题。
修改后的关键代码:
// ContentView中的ForEach部分 ForEach($viewModel.assetsRows) { $assetsRow in VStack { Button(action: { viewModel.deleteSelected(rowId: assetsRow.id) }, label: { HStack { Image(systemName: "trash") Text("Delete row") } }) RowView(assetsRow: $assetsRow) } } // ViewModel的删除方法 func deleteSelected(rowId: UUID) { withAnimation { assetsRows.removeAll { $0.id == rowId } } }
- 确保模型协议实现一致性
当前AssetItem的Equatable和Hashable仅基于id判断,该实现是合理的,但后续若新增属性,需同步更新这两个协议的实现,避免SwiftUI视图身份判断错误。
内容的提问来源于stack exchange,提问作者Mad Man
相关产品推荐
相关产品推荐

