ForEach 中的 SwiftUI 索引超出范围

Posted

技术标签:

【中文标题】ForEach 中的 SwiftUI 索引超出范围【英文标题】:SwiftUI Index out of range in ForEach 【发布时间】:2020-12-19 23:09:28 【问题描述】:

经过数小时的调试,我发现错误出现在文件夹 ContentViewsMenuItemView 的 foreach 循环内。

应用程序崩溃,错误是:

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444.

信息:

我有一个ObservableObject 内部有一个ArrayStructs 作为数据存储。

问题:

ForEach 介于 0 和数组计数 + 1 之间。这样我就可以有一个额外的项目来添加新元素。在 ForEach 中检查索引是否在边界内(if (idx >= palettesOO.palettes.count) 然后显示加号)。

但是当我右键单击任何单元格并单击“删除”时它会崩溃。这将调用类Manager 中的函数RemovePalette。在那里,数据从ObservableObject 内的数组中删除 - 这也有效。

函数被调用后,应用程序崩溃(我知道这一点,因为我在函数调用后打印了一条消息)。我发现当视图被重绘(更新)时会发生崩溃。

如果我有一个不需要绑定的视图元素,例如 Text,那么它可以工作,如果它需要绑定,例如 TextField,它就会崩溃。 Text(palettesOO.palettes[idx].palName) 在 ForEach 内的 else 内有效,但需要绑定的视图元素或子视图不起作用:TextField("", text: $palettesOO.palettes[idx].palName) 崩溃。

我尝试使用 these 之类的内容修改 ForEach,但没有成功。

代码和数据:

class PalettesOO: ObservableObject 
    @Published var palettes = [Palette]()

MenuItemView:

struct MenuItemView: View 
    @ObservedObject var palettesOO = PalettesOO()
    
    var body: some View 
        VStack 
            SectionView("Palettes") 
                LazyVGrid(columns: Array(repeating: GridItem(.fixed(viewCellSize), spacing: viewCellSpacing), count: viewColCount), spacing: viewCellSpacing) 
                    ForEach(0..<palettesOO.palettes.count + 1, id: \.self)  idx in
                        if (idx >= palettesOO.palettes.count) 
                            Button(action: 
                                newPalettePopover = true
                            , label: 
                                Image(systemName: "plus.square").font(.system(size: viewCellSize))
                            ).buttonStyle(PlainButtonStyle())
                        
                        else 
                            // Works
                            Text(palettesOO.palettes[idx].palName)
                            // Does not work
                            TextField("ASD", text: $palettesOO.palettes[palettesOO.palettes.count - 1].palName).frame(width: 100, height: 100).background(Color.red).contextMenu(ContextMenu(menuItems: 
                                Button(action: , label: 
                                    Text("Rename")
                                )
                            Button(action:  Manager.RemovePalette(name: palettesOO.palettes[idx].palName); print("Len \(palettesOO.palettes.count)") , label: 
                                    Text("Delete")
                                )
                            ))
                            // Original code, also crashes (PalettePreviewView is a custom subview which does not matter for this)
//                            PalettePreviewView(palette: $palettesOO.palettes[palettesOO.palettes.count - 1], colNum: $previewColCount, cellSize: $viewCellSize).cornerRadius(viewCellSize / 100 * viewCellRadius).contextMenu(ContextMenu(menuItems: 
//                                    Button(action: , label: 
//                                        Text("Rename")
//                                    )
//                                Button(action:  Manager.RemovePalette(name: palettesOO.palettes[idx].palName); print("Len \(palettesOO.palettes.count)") , label: 
//                                        Text("Delete")
//                                    )
//                                ))
                        
                    
                
            
        .padding().fixedSize()
    

经理:

class Manager 
    static func RemovePalette(name: String) 
        var url = assetFilesDirectory(name: "Palettes", shouldCreate: true)
        url?.appendPathComponent("\(name).json")
        if (url == nil) 
            return
        

        do 
            try FileManager.default.removeItem(at: url!)
         catch let error as NSError 
            print("Error: \(error.domain)")
        
        LoadAllPalettes()
        UserDefaults.standard.removeObject(forKey: "\(k_paletteIndicies).\(name)")
    

我知道这样复杂的问题不适合在 Stack Overflow 上发布,但我想不出其他办法。

项目版本控制在我的GitHub 上是公开的,以防需要找到解决方案。

编辑 2020 年 12 月 21 日 @ 晚上 8:30: 感谢@SHS,它现在就像一个魅力! 这是最终的工作代码:

struct MenuItemView: View 
    @ObservedObject var palettesOO = PalettesOO()
    
    var body: some View 
        VStack 
            ...
            ForEach(0..<palettesOO.palettes.count + 1, id: \.self)  idx in
                ...
                ////  @SHS Changed :-
                Safe(self.$palettesOO.palettes, index: idx)  binding in
                    TextField("ASD", text: binding.palName).frame(width: 100, height: 100).background(Color.red).contextMenu(ContextMenu(menuItems: 
                        Button(action: , label: 
                            Text("Rename")
                        )
                        Button(action:  Manager.RemovePalette(name: binding.wrappedValue.palName); print("Len \(palettesOO.palettes.count)") , label: 
                            Text("Delete")
                        )
                    ))
                
            
        
        ...
    


////  @SHS Added :-
//// You may keep the following structure in different file or Utility folder. You may rename it properly.
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View 
    
    typealias BoundElement = Binding<T.Element>
    private let binding: BoundElement
    private let content: (BoundElement) -> C
    
    init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) 
        self.content = content
        self.binding = .init(get:  binding.wrappedValue[index] ,
                             set:  binding.wrappedValue[index] = $0 )
    
    
    var body: some View 
        content(binding)
    

【问题讨论】:

如果您不打算显示代码行,您在寻求什么样的帮助?如果您尝试在 ForEach 循环中删除一条记录,显然,无论如何,应用程序都会崩溃。 我不能在这里显示代码,因为这太多了,问题太复杂了,对不起。但代码在我的GitHub 【参考方案1】:

根据Answer at *** link

创建一个结构体

struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View 
   
   typealias BoundElement = Binding<T.Element>
   private let binding: BoundElement
   private let content: (BoundElement) -> C

   init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) 
      self.content = content
      self.binding = .init(get:  binding.wrappedValue[index] ,
                           set:  binding.wrappedValue[index] = $0 )
   
   
   var body: some View 
      content(binding)
   

然后包装您的代码以访问它,如下所示

Safe(self.$palettesOO.palettes, index: idx)  binding in
    //Text(binding.wrappedValue.palName)
    TextField("ASD", text: binding.palName)
    //TextField("ASD", text: $palettesOO.palettes[palettesOO.palettes.count - 1].palName)
       .frame(width: 100, height: 100).background(Color.red)
       .contextMenu(ContextMenu(menuItems: 
             Button(action: , label: 
                 Text("Rename")
             )
             Button(action:  Manager.RemovePalette(name: binding.wrappedValue.palName); print("Len \(palettesOO.palettes.count)") , label: 
                 Text("Delete")
             )
       ))
    

我希望这可以帮助你(直到它在 Swift 中得到纠正)

【讨论】:

我已尝试实施您的建议,但它对我不起作用,我用我尝试过的代码更新了我的问题 @MarioElsnig 我已通过电子邮件向您发送了“MenuItemView.swift”。请看一下。 我刚刚试了一下,效果很好,非常感谢您的耐心等待!

以上是关于ForEach 中的 SwiftUI 索引超出范围的主要内容,如果未能解决你的问题,请参考以下文章

删除最后一个数组元素时,SwiftUI列表ForEach索引超出范围

Swiftui foreach 索引超出范围 ondelete 问题

索引超出范围 SwiftUI

SwiftUI .onDelete 抛出致命错误:索引超出范围

SwiftUI - 索引超出范围

SwiftUI - 致命错误:从数组中删除元素时索引超出范围