使用 ForLoop SwiftUI 在两层惰性堆栈中获取“致命错误索引超出范围”

问题描述 投票:0回答:1

我有一个元素数组,其中包含其他元素的嵌套数组。 删除数组的一行时,有时会发生崩溃,并显示消息“Swift/ContigeousArrayBuffer.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)
    }
}

尝试将

@Binding
中的
@State
更改为
RowView
,可以防止崩溃,但是
isSelected
无法正常工作,因为它没有与viewModel的值“绑定”。

我猜这是一个内部 SwiftUI 错误。 (Xcode 15.4、iOS 17+)

ios swift xcode swiftui nested-for-loop
1个回答
0
投票

尝试使用

ForEach($viewModel.assetsRows)
而不使用
id: \.self
的方法 以及用于绑定的
$assetsRow
withAnimation
中也没有
ViewModel
withAnimation
仅在用于
View
时才有意义。

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
         ScrollView {
            LazyVStack {
                ForEach($viewModel.assetsRows) { $assetsRow in  // <--- here
                    VStack {
                        Button(action: {
                            viewModel.deleteSelected(assetsIn: assetsRow)
                        }, label: {
                            HStack {
                                Image(systemName: "trash")
                                Text("Delete row")
                            }
                        })
                        RowView(assetsRow: $assetsRow)  // <--- here
                    }
                }
            }
        }
    }
}

struct RowView: View {
    @Binding var assetsRow: AssetsRowModel

    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach($assetsRow.items) { item in  // <--- here
                    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(AssetsRowModel(items: [AssetItem(), AssetItem(), AssetItem()]))
        }
        return array
    }()

    // removing items causes crash (not 100% times)
    func deleteSelected(assetsIn row: AssetsRowModel) {
        // <-- no 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)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.