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)
    }
}
2
  • 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. Commented Dec 19, 2020 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 Commented Dec 19, 2020 at 23:26

1 Answer 1

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 )

Sign up to request clarification or add additional context in comments.

3 Comments

I have tried to implement what you suggested but it did not work for me, I updated my question with the code I tried it with
@MarioElsnig I have emailed you 'MenuItemView.swift'. Please take a look at it.
I've just tried it and it worked, thank you so much for your patience!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.