2

I have the following code for a SwiftUI list with children, adapted from another StackOverflow answer:

SwiftUI 2.0 List with children - how to make the tappable area of the disclosure button cover the whole list item

struct ContentView: View {

    @EnvironmentObject var goodies: GlobalGoodies
    
    func listItem(for item: User) -> some View {
        Text(item.name)
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(Array(goodies.users.enumerated()), id: \.1.id) { i, group in
                    DisclosureGroup(isExpanded: $goodies.users[i].isExpanded) {
                        ForEach(group.children ?? []) { item in
                            listItem(for:item)
                        }
                        .opacity(goodies.users[i].opacity)
                    } label: {
                        listItem(for: group)
                        .contentShape(Rectangle())
                        .onTapGesture {
                            withAnimation {
                                goodies.users[i].opacity = goodies.users[i].isExpanded ? 0.0 : 1.0
                                goodies.users[i].isExpanded.toggle()
                            }
                        }
                    }
                }
                .onDelete(perform: delete)
            }
        }
    }

    func delete(at offsets: IndexSet) {
        goodies.users.remove(atOffsets: offsets)
    }
}

Source code for the GlobalGoodies class and User class, adapted from a HackingWithSwift tutorial example.

struct User: Identifiable {
    let id = UUID()
    var name: String
    var children: [User]? = nil
    
    var isExpanded = false
    var opacity = 0.0
}

class GlobalGoodies: ObservableObject {
    @Published var users = [
        User(name: "Paul", children: [
            User(name: "Jenny")
        ]),
        User(name: "Taylor", children: [
            User(name: "Tyler"),
            User(name: "Luna")
        ]),
        User(name: "Adele", children: nil)
    ]
}

I am able to view these elements normally, and I'm able to delete two (parent) items without any trouble. However, when I try to delete the last remaining (parent) element in the list, the app crashes with this message:

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

I'm aware others have ran into similar errors with the same message when using List but this error occurs only when using this particular setup for showing expandable child elements. I followed the above mentioned HackingWithSwift tutorial's code (which is just a normal linear list) and I am able to delete all the elements in it with no issue.

I've tried using the new List(children:) however I'm not able to add onDelete to that for some reason.

Any help pinpointing the location of this error would be great, as I've tried putting breakpoints, etc. to see where things go wrong. The element gets deleted nominally, but the crash seems to happen either during or after the call to body.

Thanks in advance for any help!

themathsrobot
  • 581
  • 6
  • 20

1 Answers1

2

I seem to have been able to figure this out myself after a little more probing around. The out-of-range error occurred on this line:

DisclosureGroup(isExpanded: $goodies.users[i].isExpanded) {

(I was able to tell by replacing with .constant(true) which stopped the crash.)

I was able to fix this by using a function that gives a custom binding given the index i, which checks that the index is in range before accessing it:

func binding(for index: Int) -> Binding<Bool> {
        Binding<Bool>(
            get: {
                if index > goodies.users.count - 1{
                    return false
                }
                return goodies.users[index].isExpanded
            },
            set: {
                if index > goodies.users.count - 1 {
                    // do nothing
                } else {
                    goodies.users[index].isExpanded = $0
                }
            }
        )
}

Usage for the affected line:

DisclosureGroup(isExpanded: binding(for: i)) {

This fixes the crashing issue. It was a little hard to pinpoint before because I didn't realise the binding was accessed after the list item was removed. Hope this helps someone in the same position.

themathsrobot
  • 581
  • 6
  • 20