Возвращайте счетчик SwiftUI @FetchedResults из дочернего представления в родительское при изменении фильтра предикатов @fetchRequest

#swiftui #count #binding #predicate #fetchrequest

Вопрос:

Родительское представление отправляет дочернему представлению строку фильтра предикатов для a FetchRequest и a Binding для возврата FetchedResults количества.

Строка фильтра является @State свойством родительского представления. Изменение строки фильтра в родительском элементе приводит к тому, что дочерний элемент обновляет FetchRequest .

Как я могу заставить дочернее представление обновить Binding полученное с помощью нового FetchedResults счетчика?

Смотрите Комментарии к коду в Child моих попытках.

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

Ответ №1:

есть 2 вещи, которые мне нужно было сделать, чтобы ваш код работал. Сначала вам нужно обновить фильтр в дочернем представлении при изменении фильтра в родительском. Это делается в моем коде с использованием традиционной привязки. Затем, поскольку onAppear происходит в основном только один раз, я использовал onReceive для обновления счета выборки. Вот мой код:

РЕДАКТИРОВАТЬ: с предложениями от @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()
        }
    }
}

  
 

Комментарии:

1. .onRecieve(fetchRequest.publisher) кажется, это отличное решение, за исключением одного случая: когда фильтр FetchRequest не возвращает результатов, счетчик не обновляется в родительском файле. Знаете ли вы, как заставить издателя опубликовать событие, даже если основные данные не дают результатов?

2. попробуйте это: .onReceive(fetchRequest.publisher.count()) {...} . Я обновлю свой ответ, если он вас устроит.

3. Это работает! Обновляя ваш ответ, мне не нужно было import Combine или использовать Binding вход Child для фильтра (так как дочернее представление не записывается в фильтр). Простое добавление .onRecieve сделало свое дело.

4. Обновил свой ответ. Если это ответ на ваш вопрос, пожалуйста, отметьте его как правильный галочкой, спасибо.