Search code examples
iosswiftswiftuiswiftdata

How to write Query with Predicate in SwiftUI detail view


I'm trying to write a @Query with a custom Predicate in a SwiftUI detail view. When I click items in the list the app hangs and memory usage increases linearly forever.

If I put the predicate on the @Query in the main view, the app works fine.

This is all based on the SwiftUI+SwiftData template app with extremely minimal modifications in the detail view:

Detail view (where issue occurs):

struct DetailView: View {
    var item: Item

    // Issue occurs when a Query with a filter is set. Without the filter it works fine
    // contents of Predicate doesn't matter so long as it's valid
    @Query(filter: #Predicate<Item> { item in
        true
    }) private var items: [Item]

    var body: some View {
        VStack {
            Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
            Text("Count: \(items.count)")
        }
    }
}

List view:

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext

    // If this Query is modified, no issue
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        DetailView(item: item)
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }
}

Data model:

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

App runtime:

@main
struct TestSwiftDataApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

Solution

  • This is not a documented way of using NavigationSplitView. You should use a List selection to control when the detail view is shown.

    @Query private var items: [Item]
    @State private var selectedItem: PersistentIdentifier?
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItem) {
                ForEach(items) { item in
                    NavigationLink(value: item.id) {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
            }
        } detail: {
            if let selectedItem, let item: Item = modelContext.registeredModel(for: selectedItem) {
                DetailView(item: item)
            } else {
                Text("Select an item")
            }
        }
    

    If you are using NavigationStack instead, using the value-based navigationDestination(for:) fixes the issue.

    NavigationStack {
        List {
            ForEach(items) { item in
                NavigationLink(value: item) {
                    Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                }
            }
        }
        .navigationDestination(for: Item.self) { item in
            DetailView(item: item)
        }
    }