Hi there,
I'm running into a weird bug that I just for the life of me cannot pinpoint. This bug only occurs in Ventura for some reason, this doesn't occur in Monterrey.
I have a macOS receipt management app made in SwiftUI where you can add receipts and receipt groups. However it seems that when I delete the last receipt or receipt group in their respective arrays, the app crashes and I get this error:
2022-10-30 10:45:53.758242-0700 Totaly[25035:1655580] Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range
I've debugged this through and through on Ventura and the element of the array I delete a receipt group from is deleted properly, meaning this error does not occur when deleting a receipt group. The actual error seems to trigger several steps after the receipt group has been deleted, when exiting a function that calls the delete function.
Here's the process of how deleting works in code:
When the user right clicks on a receipt group and clicks "Delete" this code runs. Basically a confirmation dialog pops asking if the user want to delete the receipt group.
Snippet 1:
Button("Delete") {
modelData.openConfirmationDialog(title: "Delete \(receiptGroup.name)?") {
do {
try modelData.deleteReceiptGroup(receiptGroup: receiptGroup)
Task { try await modelData.saveToDisk() }
modelData.updateCurrentReceipt()
} catch {
modelData.openAlert()
}
}
}
The openConfirmationDialog method is a method I've made that that takes takes what you want to dialog to say, and a function that runs if the user clicks yes.
Snippet 2:
func openConfirmationDialog(title: String, funcToRunIfYes: @escaping () -> () = {}) {
confirmationDialogTitle = title
confirmationDialogFunc = funcToRunIfYes
showConfirmationDialog = true
}
confirmationDialogFunc is a variable that holds a function that is tied to the ModelData class:
Snippet 3:
var confirmationDialogFunc = {}
Then in my ContentView.swift, this modifier is appended to a NavigationView, and is where the closure passed into openConfirmationDialog is run
Snippet 4:
.confirmationDialog(modelData.confirmationDialogTitle, isPresented: $modelData.showConfirmationDialog) {
Button("Yes") {
modelData.confirmationDialogFunc()
}
Button("No", role: .cancel) {}
}
The closure from Snippet 1 is now run, calling the deleteReceiptGroup method, passing in the currently selected receipt group. Here I delete the directory that contains the receipt images, as well as delete the receipt group from the receiptGroups array. Lines 3 and 4 of this method ARE properly working, and in debugging, my app is finding the correct receipt group, and deleting it properly. Line 4 is not where the out of range error occurs.
Snippet 5:
func deleteReceiptGroup(receiptGroup: ReceiptGroup) throws {
guard let directoryUrl = receiptGroup.directoryUrl else { return }
try FileManager.default.removeItem(at: directoryUrl)
guard let index = self.receiptGroups.firstIndex(where: {$0.id == receiptGroup.id}) else { return }
self.receiptGroups.remove(at: index)
}
Finally my app exits the Button("Yes")
block of code from snippet 4, which is then where the error triggers:
My hypothesis so far is something weird is happening in a different thread and it tries to access the last element when it doesn't exist anymore, but thats's really just a wild guess at this point. I am calling these methods through multiple closures so that's really all I can think of.
Again I want to point out this isn't occurring on Monterey at all, only Ventura. Maybe apple changed something in SwiftUI that's causing this occur?
Let me know what you guys think.
Thanks!
For future folks reading this, I figured it out! I'm still not sure why I was getting the index out of range error, but I do know how it was occurring.
Essentially I was iterating through my receipt groups in a ForEach using a binding like this:
ForEach($modelData.receiptGroups) { $receiptGroup in
return GroupDisclose(receiptGroup: $receiptGroup)
}
This works fine if the array in question contains value types, however in my case a receipt group is a reference type so I was essentially passing a binding to a reference type to my view GroupDisclose, which in turn had a DisclosureGroup in it, which takes a binding to a value to determine whether or not the disclosure group is open. SwiftUI does not like the @Binding
property wrapper on reference types and is why I think I was getting the error which was crashing my app.
To fix this, there's a couple things you can do. First, you need to make sure that if you DO need a binding to a reference type, that you make your type conform to the ObservableObject protocol and use the @ObservedObject
property wrapper before declaring your variable. You can then use it in views that require bindings. Second make sure you don't use ForEach with bindings. Since they are reference types already you don't need them to be bindings to mutate them. This will work fine:
ForEach(modelData.receiptGroups) { receiptGroup in
return GroupDisclose(receiptGroup: receiptGroup)
}
These fixes worked for me, hopefully they work for you too!