I'm trying to determine if this is "expected" swiftui behavior or an issue with SwiftUI/Data which needs a feedback request...
When a child view has a Query containing a filter predicate, the query is run with each and every edit of the parent view, even when the edit has no impact on the child view (e.g. bindings not changing).
In the example below, ContentView has the TextField name, and while data is being entered in it, causes the Query in AddTestStageView to be run with each character typed, e.g. 30 characters result in 30 query executions.
(Need "-com.apple.CoreData.SQLDebug 1" launch argument to see SQL output).
Removing the filter predicate from the query and filtering in ForEach prevents the issue.
In my actual use case, the query has a relatively small result set (<100 rows), but I can see this as a performance issue with the larger result sets.
xcode/ios: 26.2
Repro example code:
import SwiftUI
import SwiftData
// Repro to Query filter issue in child view running multiple time unexpectedly
// Need "-com.apple.CoreData.SQLDebug 1" launch argument set to see SQL console output.
@main
struct ReproViewQueryMultipleRunningsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(DataManager.shared.sharedModelContainer())
}
}
@Model
final class TestStageClass {
var id: UUID = UUID()
var name: String = ""
var isActive: Bool = true
var displayOrder: Int = 0
init(name: String, isActive: Bool, displayOrder: Int) {
self.name = name
self.isActive = isActive
self.displayOrder = displayOrder
}
}
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var name: String = ""
@State private var selectedTestStage: TestStageClass = DataManager.shared.getFirstTestStageClass()
var body: some View {
VStack (spacing: 20) {
TextField("Name", text: $name)
AddTestStageView(selectedTestStage: $selectedTestStage)
}
.frame(height: 200)
}
}
#Preview("Sample Data") {
ContentView()
.modelContainer(DataManager.shared.sharedModelContainer())
}
struct AddTestStageView: View {
@Environment(\.modelContext) var modelContext
@Binding var selectedTestStage: TestStageClass
// MARK: - ISSUE LOCATION
/// Using this Query with filter causes it to be run after each editing on parent view - such as each letter when editing a name.
@Query(filter: #Predicate<TestStageClass> { $0.isActive }) private var testStageClasses: [TestStageClass]
/// Using this query doesn't have the issue, then need filter in ForEach.
// @Query() private var testStageClasses: [TestStageClass]
var body: some View {
Picker("stage", selection: $selectedTestStage) { // filter and sort here does not affect issue with above Query predicate filter.
ForEach(testStageClasses.filter(\.isActive).sorted(by: { $0.displayOrder < $1.displayOrder } ), id: \.id) { stage in
Text("\(stage.name)")
.tag(stage)
}
}
}
}
class DataManager {
static let shared = DataManager()
private var modelContainer: ModelContainer? = nil
public func sharedModelContainer(inMemory: Bool = false) -> ModelContainer {
let schema = Schema([TestStageClass.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: inMemory)
do {
self.modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
checkDataExists()
return self.modelContainer!
} catch {
fatalError("Could not create sharedModelContainer. Schema:\(schema.entities.map(\.name)), \((modelConfiguration.isStoredInMemoryOnly) ? "in memory only" : "in disk"):\n\(error.localizedDescription)")
}
}
private func checkDataExists() {
let mainContext = self.modelContainer!.mainContext
print("checkDataExists")
do {
let classData: [TestStageClass] = try mainContext.fetch(FetchDescriptor<TestStageClass>())
if classData.isEmpty {
mainContext.insert(TestStageClass(name: "Beginning", isActive: true, displayOrder: 0))
mainContext.insert(TestStageClass(name: "Second Middle", isActive: false, displayOrder: 2))
mainContext.insert(TestStageClass(name: "Middle", isActive: true, displayOrder: 1))
mainContext.insert(TestStageClass(name: "End", isActive: true, displayOrder: 3))
}
if mainContext.hasChanges {
try? mainContext.save()
print("Added Default Data for TestStageClass")
}
} catch {
fatalError("Failed to get item count for TestStageClass: \(error.localizedDescription)")
}
}
func getFirstTestStageClass() -> TestStageClass {
let mainContext = self.modelContainer!.mainContext
var tmp: TestStageClass?
do {
let classData: [TestStageClass] = try mainContext.fetch(FetchDescriptor<TestStageClass>())
tmp = classData.sorted(by: {$0.displayOrder < $1.displayOrder }).first
} catch {
fatalError("getFirstTestStageClass: \(error.localizedDescription)")
}
return tmp!
}
}
Thanks, Steve