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
Okay, so it turned out my problem here was nothing to do with the arrays.
The problem was in
StudentCardView
, where I had:There was nothing in this View that told it to update itself based on changes in the Student object.
Changing it to:
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. 🤷🏻♂️You could try a different approach, using the
filter
directly in theList
, instead ofcreating separate computed variables,
like in this example code:
Note, a computed var can be used, and when a new
student
is added somewhere, theView
will be updated,as long as the
students
areState
orStateObject/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:
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.
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…
Faster than filtering by Standard Library is filtering directly in Core Data with a predicate.
FetchedResults
has annsPredicate
property. When set it updates the associated propertyA simple solution is to add a
State
propertygrade
In the
onChange
modifier of the view change the predicate. You can define a value (here 0) to show all records