0

After hours of debugging I figured out the error is inside the foreach loop in MenuItemView in the folder ContentViews.

The app crashes and the error is:

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

Information:

I have got an ObservableObject with an Array of Structs inside as data storage.

The problem:

The ForEach goes between 0 and the array count + 1. This is so I can have an extra item for adding new elements. In the ForEach is a check if the index is inside the bounds (if (idx >= palettesOO.palettes.count) then show the plus).

But it crashes when I right click any cell and click "Remove". This calls the function RemovePalette in the class Manager. There the data gets removed from the array inside the ObservableObject - this also works.

AFTER the function gets called the app crashes (I know this because I printed a message after the function call). I figured out that the crash occurs when the view gets redrawn (updated).

If I have a view element which does not need a binding, for example a Text, then it works, if it needs a binding, for example a TextField it crashes. Text(palettesOO.palettes[idx].palName) inside of the else inside the ForEach works but view elements or subviews which require Bindings do not work: TextField("", text: $palettesOO.palettes[idx].palName) crashes.

I have tried modifying the ForEach with things like these but with no success.

The Code and Data:

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()
    }
}

Manager:

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)")
    }
}

I know that such complex problems are not good to post on Stack Overflow but I can't think of any other way.

The project version control is public on my GitHub, in case it's needed to find a solution.

EDIT 12/21/2020 @ 8:30pm: Thanks to @SHS it now works like a charm! Here is the final working code:

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)
    }
}
Mario Elsnig
  • 135
  • 1
  • 8
  • What kind of help are you seeking if you are not going to show lines of code? If you try to delete a record inside the ForEach loop, obviously, the app will crash, anyway. – El Tomato Dec 19 '20 at 23:23
  • I can't show the code here because that would be to much and the problem is too complex for that, I'm sorry. But the code is on my [GitHub](https://github.com/MarioMatschgi/MenuColorPalettes) – Mario Elsnig Dec 19 '20 at 23:26

1 Answers1

5

As per Answer at stackoverflow link

Create a struct as under

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)
   }
}

Then wrap your code for accessing it as under

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")
             })
       }))
    }

I hope this can help you ( till it is corrected in Swift )

SHS
  • 1,414
  • 4
  • 26
  • 43