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()
}
}
}
}
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
}
)