The parent view sends the child view a predicate filter string for a FetchRequest
and a Binding
to return the FetchedResults
count.
The filter string is an @State
property of the parent view. A change to the filter string in the parent causes the child to update the FetchRequest
.
How can I have the child view update the Binding
it receives with the new FetchedResults
count?
See code comments in Child
for my attempts.
struct Parent: View {
@State private var filter = "" // Predicate filter
@State private var fetchCount = 0 // To be updated by child
var body: some View {
VStack {
// Create core data test records (Entity "Item" with a property named "name")
Button("Create Test Items") {
let context = PersistenceController.shared.container.viewContext
let names = ["a", "ab", "aab", "b"]
for name in names {
let item = Item(context: context)
item.name = name
try! context.save()
}
}
// Buttons to modify the filter to update the fetch request
HStack {
Button("Add") {
filter = filter + "a"
}
Button("Remove") {
filter = String(filter.prefix(filter.count-1 >= 0 ? filter.count-1 : 0))
}
Text("Filter: \(filter)")
}
Text("Fetch count in parent view (not ok): \(fetchCount)")
Child(filter: filter, fetchCount: $fetchCount)
Spacer()
}
}
}
struct Child: View {
@FetchRequest var fetchRequest: FetchedResults<Item>
@Binding var fetchCount: Int
init(filter: String, fetchCount: Binding<Int>) {
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", filter)
self._fetchRequest = FetchRequest<Item>(
entity: Item.entity(),
sortDescriptors: [],
predicate: filter.isEmpty ? nil : predicate
)
self._fetchCount = fetchCount
// self.fetchCount = fetchRequest.count // "Modifying state during view updates, this will cause undefined behavior"
}
var body: some View {
VStack {
Text("Fetch count in child view (ok): \(fetchRequest.count)")
.onAppear { fetchCount = fetchRequest.count } // Fires once, not whenever the predicate filter changes
// The ForEach's onAppear() doesn't update the count when the results are reduced and repeatedly updates for every new item in results
ForEach(fetchRequest) { item in
Text(item.name ?? "nil")
}
//.onAppear { fetchCount = fetchRequest.count }
}
}
}
there are 2 things that I needed to do, to make your code work. First you need to update the filter in the Child view when you change the filter in the Parent. This is done in my code using the traditional Binding. Then, since onAppear only happens mostly once, I used onReceive to update the fetchCount. Here is my code:
EDIT: with suggestions from @Jesse Blake
import Foundation
import SwiftUI
struct Child: View {
@FetchRequest var fetchRequest: FetchedResults<Item>
@Binding var fetchCount: Int
var filter: String // <-- here
init(filter: String, fetchCount: Binding<Int>) {
self.filter = filter // <-- here
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", filter)
self._fetchRequest = FetchRequest<Item>(
entity: Item.entity(),
sortDescriptors: [],
predicate: filter.isEmpty ? nil : predicate
)
self._fetchCount = fetchCount
}
var body: some View {
VStack {
Text("Fetch count in child view (ok): \(fetchRequest.count)")
ForEach(fetchRequest) { item in
Text(item.name ?? "nil")
}
}
// in older ios
// .onChange(of: fetchRequest.count) { newVal in // <-- here
// fetchCount = fetchRequest.count
// }
.onReceive(fetchRequest.publisher.count()) { _ in // <-- here
fetchCount = fetchRequest.count
}
}
}
struct Parent: View {
@Environment(\.managedObjectContext) private var viewContext
@State private var filter = "" // Predicate filter
@State private var fetchCount = 0 // To be updated by child
var body: some View {
VStack (spacing: 50) {
// Create core data test records (Entity "Item" with a property named "name")
Button("Create Test Items") {
let context = PersistenceController.shared.container.viewContext
let names = ["a", "ab", "aab", "b"]
for name in names {
let item = Item(context: context)
item.name = name
try! context.save()
}
}
// Buttons to modify the filter to update the fetch request
HStack {
Button("Add") {
filter = filter + "a"
}
Button("Remove") {
filter = String(filter.prefix(filter.count-1 >= 0 ? filter.count-1 : 0))
}
Text("Filter: \(filter)")
}
Text("Fetch count in parent view (not ok): \(fetchCount)")
Child(filter: filter, fetchCount: $fetchCount) // <-- here
Spacer()
}
}
}