0

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 }
        }
    }
}
jesseblake
  • 100
  • 1
  • 10

1 Answers1

0

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

  
  • `.onRecieve(fetchRequest.publisher)` seems to be a great solution except for one case: when the filter causes the `FetchRequest` to return no results, the count isn't updated in the parent. Do you know how to get the publisher to publish an event even if core data returns no results? – jesseblake Aug 08 '21 at 22:49
  • try this: `.onReceive(fetchRequest.publisher.count()) {...}`. I'll update my answer if it works for you. – workingdog support Ukraine Aug 09 '21 at 02:27
  • It works! Re updating your answer, I didn't need to `import Combine` or use a `Binding` in `Child` for the filter (since the child view doesn't write to the filter). Just adding `.onRecieve` did the trick. – jesseblake Aug 09 '21 at 02:51
  • Updated my answer. If this answers your question please mark it as correct with the tick mark, thank you. – workingdog support Ukraine Aug 09 '21 at 03:25