skip to Main Content

In a SwiftUI App using Core Data, and I have a FetchRequest in the top level View:

    @FetchRequest(fetchRequest: Student.fetchRequest())
    private var students: FetchedResults<Student>

And then I have some computed property arrays that I derive from that FetchResults:

    private var year7Students: [Student] {
        return students.filter { $0.grade == 7 }
    }

    private var year8Students: [Student] {
        return students.filter { $0.grade == 8 }
    }
    ...

If I make a View with a List over the students: FetchedResults, like this:

    var body: some View {
        NavigationView {
            List {
                Section("All Students") {
                    ForEach(students) { student in
                        NavigationLink(destination: StudentDetailView(student: student) {
                            StudentCardView(student: student)
                        }
                    }
                }
             }
         }
     }

then it shows all the students, and when I update one (via the detail View), the View automatically updates.

But when I do the same using year7Students: [Student] etc, like this:

            List {
                Section("Year 7 Students") {
                    ForEach(year7Students) { student in
                        NavigationLink(destination: StudentDetailView(student: student) {
                            StudentCardView(student: student)
                        }
                    }
                }
                Section("Year 8 Students") {
                    ForEach(year8Students) { student in
                        NavigationLink(destination: StudentDetailView(student: student) {
                            StudentCardView(student: student)
                        }
                    }
                }
                ...
             }

then it shows the relevant students in each list, but updates made in the Detail view do not result in the data in the List being updated when the nav stack pops back to this view, which I’m guessing means a View refresh isn’t being triggered.

What can I do to make the Views based on the computed property arrays reactive to when the objects in the underlying students property changes?
Or what different approach can I use to achieve the same effect?

4

Answers


  1. Chosen as BEST ANSWER

    Okay, so it turned out my problem here was nothing to do with the arrays.

    The problem was in StudentCardView, where I had:

        let student: Student
    

    There was nothing in this View that told it to update itself based on changes in the Student object.

    Changing it to:

        @ObservedObject var student: Student
    

    made everything start working. So it seems arrays that are computed properties based off FetchResults are observed by the View without doing any extra work. 🤷🏻‍♂️


  2. You could try a different approach, using the filter directly in the List, instead of
    creating separate computed variables,
    like in this example code:

    struct Student: Identifiable {
        let id = UUID()
        var name: String
        var grade: Int
    }
    
    struct ContentView: View {
        @State var gradeFilter: Int = 0
        
        // for testing
        @State var students = [Student(name: "student-1", grade: 1),
                               Student(name: "student-2", grade: 7),
                               Student(name: "student-3", grade: 7),
                               Student(name: "student-4", grade: 8)]
        
        var body: some View {
            VStack {
                List(students.filter {$0.grade == gradeFilter}) { student in
                    Text(student.name)
                }
                HStack {
                    Button("set filter to 7") {  gradeFilter = 7 }
                    Spacer()
                    Button("set filter to 8") {  gradeFilter = 8 }
                }
                HStack {
                    Button("add a student with grade 7") {
                        students.append(Student(name: "student-7", grade: 7))
                    }
                    Spacer()
                    Button("add a student with grade 8") {
                        students.append(Student(name: "student-5", grade: 8))
                    }
                }
            }.buttonStyle(.bordered)
            .padding()
        }
        
    }
    

    Note, a computed var can be used, and when a new student is added somewhere, the View will be updated,
    as long as the students are State or StateObject/ObservedObject (CoreData objects are ObservedObject),
    that is, is under observation by the view.

    Similarly, if something else, like @State var gradeFilter is changed,
    the View will be updated.

    For example:

     var filteredStudents: [Student] {
         students.filter { $0.grade == gradeFilter }
     }
     
     //...
     List(filteredStudents) { student in
         Text(student.name)
     }
     //...
     
    
    Login or Signup to reply.
  3. Following from my comment I did some code test. You need the filters to return as a FetchedResults type. They are then objects in the managed object context and updates will be picked up. To do this you need to run another request that applies an NSPredicate as the filter. This will not actually load again from the "database" as values should already be cached within the context.

    You could use the main FetchRequest but also use another variable as the one that filters in case you use the full list elsewhere at the same time.

    var studentsByGrade: @FetchRequest( sortDescriptors: [] , predicate: nil) private var subSet: FetchedResults<Item>
    
    func getStudentsByGrade(_ grade: String) {
        let predicate = NSPredicate(format: "grade == %@" , grade )
        studentsByGrade.nsPredicate == predicate
    }
    

    This will update the predicate (filter) and cause it to refresh (which will be cached so quick). The link to the Apple documentation on FetchRequest.predicate – https://developer.apple.com/documentation/swiftui/fetchrequest/configuration/nspredicate

    The strings are a bit clunky as at the heart core data seems to rely on a lot of objc and strings…

    Login or Signup to reply.
  4. Faster than filtering by Standard Library is filtering directly in Core Data with a predicate.

    FetchedResults has an nsPredicate property. When set it updates the associated property

    A simple solution is to add a State property grade

    @State private var grade = 0
    

    In the onChange modifier of the view change the predicate. You can define a value (here 0) to show all records

    .onChange(of: grade) { newGrade in
        students.nsPredicate = newGrade == 0 
        ? nil 
        : NSPredicate(format: "grade = %ld", newGrade) 
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search