swiftuicountbindingpredicatefetchrequest

Return SwiftUI @FetchedResults count from child view to parent when @FetchRequest predicate filter changes


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

Solution

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