Getting a "Fatal error: Index out of range" error only in macOS Ventura

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!

Answered by Jeidoban in 735659022

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!

Effectively, looks like a synchro issue.

Did you find exactly where it crashes ?

Could you add a print statement each time you access receiptGroups in your code

print("receiptGroups", receiptGroups.count)

Could you try to wait for 1s in places where you change receiptGroups, to see if it is a race issue ?

@Claude31

I did a bit more debugging and I found that the exact place it is crashing is during rendering the sidebar (where the receipt groups show up) after deletion after "List":

var body: some View {

    print("receiptGroups in opening view", modelData.receiptGroups.count)

    return VStack {

        List(selection: $modelData.selection) { <-- Crashing here

            ForEach($modelData.receiptGroups) { $receiptGroup in

                return GroupDisclose(receiptGroup: $receiptGroup)

            }

        }

... More code below this

The print statement above is coming up with 1 receipt group left, which is correct as there was two before deletion. The correct number of receipts groups is there by the time SwiftUI attempts to re-render, so I really have no clue what's going on.

Another note: during this debugging session while stepping through, it did actually delete correctly once, so it may be a race condition. However I haven't been able to reproduce it since, stepping through now still results in the crash at the List.

So at this point I'm still super confused as to what's going on. I'm curious if Apple changed the way swiftUI renders data in Ventura and its somehow causing a race condition.

Could you try a patch, with some delay:

var body: some View {

    return VStack {

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        List(selection: $modelData.selection) { <-- Crashing here
            ForEach($modelData.receiptGroups) { $receiptGroup in
                return GroupDisclose(receiptGroup: $receiptGroup)
            }
        }
    }

May check here for other options to delay: https://stackoverflow.com/questions/59682446/how-to-trigger-action-after-x-seconds-in-swiftui

@Claude31

SwiftUI doesn't like having the DispatchQueue there, it unfortunately throws errors. Also tried adding a task to the List but I believe those are more for triggering a separate event based on it appearing rather that delaying the rendering itself. I also made my delete function async and tried waiting a second after deletion, but swiftui doesn't like updating from background threads, and it also still crashes :(

I've never ran into a bug like this before. I'm just at such a loss as to what would be causing it.

Did you try the other solutions proposed in the link ?

You're right, Dispatch does not compile.

Could you try some brute force:

        List(selection: $modelData.selection) { <-- Crashing here
            ForEach($modelData.receiptGroups) { $receiptGroup in
                return GroupDisclose(receiptGroup: $receiptGroup).onAppear() { sleep(1) ; print("Just to see")}
            }

@Claude I for some reason can't leave comments on your replies (they just never show up after submitting). In response to this reply: "Did you try the other solutions proposed in the link ?" I did what that thread suggested and appended a task modifier to the view, however that just creates a separate task and doesn't seem to delay the view rendering itself.

For your latest reply, I'll give that a try tonight after work and see if that works.

I really appreciate all the help you've given me thus far, you're the hero of this thead :)

Oops @ the wrong username. @Claude31

@Claude31 Tested it out again, and while it did actually delay rendering, it still resulted in the app crashing. I did take another pass at debugging really closely, and I can confirm its crashing right after the ForEach, after trying to render a third GroupDisclose (which the user would have just deleted).

Considering this is only happening in Ventura, I think Apple changed some code somewhere in Swift or SwiftUI that clashes with my previous code. Perhaps I implemented something weird somewhere in my code that is throwing everything off? I think I'm gonna use one of my two TSIs and see if I can get an Apple support engineer to take a look at my code, because I'm at a total loss at what would be causing this at this point.

Thank you so much Claude for your help so far, let me know if there's anything else you think I should try :)

I'll make sure to post my solution if I'm able to get it figured out with Apple

Accepted Answer

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!

Getting a "Fatal error: Index out of range" error only in macOS Ventura
 
 
Q