Search code examples
swiftuiproperty-wrapper

How to have @State that is an array with the size that depends on a FetchRequest


I have an app that is outlined in the following. Most of the code I have adapted from https://www.hackingwithswift.com/books/ios-swiftui/how-to-combine-core-data-and-swiftui

But the part I have question about is how to set @State var isListExpanded: [Bool] properly. It is an array whose size depends on the result of a FetchRequest (students). Even though the app works initially, but if I tap on "Add" it crashes with the message: Fatal error: Index out of range on this line DisclosureGroup(isExpanded: $isListExpanded[index]) { which is understandable because the size of array isListExpanded is not adjusted to the new size.

 // StudentsApp.swift

import SwiftUI
import CoreData

@main
struct StudentsApp: App {
    @StateObject private var dataController = DataController()
    var body: some Scene {
        WindowGroup {
            let request: NSFetchRequest<Student> = Student.fetchRequest()
            let students = try? dataController.container.viewContext.fetch(request)
            
            ContentView(isListExpanded: Array(repeating: false, count: students?.count ?? 0))
                .environment(\.managedObjectContext, dataController.container.viewContext)
        }
    }
}

//  DataController.swift

import Foundation
import CoreData

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "Students")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Core Data failed tp load: \(error.localizedDescription)")
            }
        }
    }
}

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: []) var students: FetchedResults<Student>
    @State var isListExpanded: [Bool]
    
    var body: some View {
        VStack {
            List {
                ForEach(students.indices, id: \.self) { index in
                    Section {
                        DisclosureGroup(isExpanded: $isListExpanded[index]) {
                            Text("Expanded")
                        } label: {
                            Text(students[index].name ?? "Unknown")
                        }
                    }
                }
            }
            Button("Add") {
                let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
                let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]
                let chosenFirstName = firstNames.randomElement()!
                let chosenLastName = lastNames.randomElement()!
                let student = Student(context: moc)
                student.id = UUID()
                student.name = "\(chosenFirstName) \(chosenLastName)"
                try? moc.save()
            }
        }
    }
}

Solution

  • After some back and forth with Bard and ChatGPT I came up with this which works fine for now.

    I replaced $isListExpanded[index] in

    DisclosureGroup(isExpanded: $isListExpanded[index]) {
        Text("Expanded")
    } label: {
        Text(students[index].name ?? "Unknown")
    }
    

    with

    Binding(
        get: { self.isListExpanded.indices.contains(index) ? self.isListExpanded[index] : false },
        set: { newValue in
            if newValue {
                while self.isListExpanded.count <= index {
                    self.isListExpanded.append(false)
                }
            }
            self.isListExpanded[index] = newValue
        } 
    )