Skip to content

juanarzola/Queried

Repository files navigation

Queried (Experimental)

@Queried is a SwiftData macro applied to an array property of a class or actor that generates a function that auto-updates it with the latest values of a FetchDescriptor. This is intended to be used to populate arrays in non-view objects.

I intend to use this as a proof of concept of how non-View queries could be implemented.

It uses an internal SwiftData notification that the @Query macro uses to update views. These may break in future SwiftData updates, so use this at your own risk!

Example (from QueriedSample):

import SwiftUI
import SwiftData
import Queried

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

@MainActor @Observable
class ContentController {
    @Queried
    var items: [Item] = []

    // call this in a `.task()` in the view to keep ContentController up-to-date
    func updates(in container: ModelContainer) async {
        do {
            for try await currItems in items(
                FetchDescriptor(predicate: .true),
                in: container.mainContext
            ) {
               print("\(#function): Got \(currItems.count) items")
            }
        } catch let error {
            print("\(#function): Error: \(error.localizedDescription)")
        }
    }
}

@MainActor
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var controller = ContentController()

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(controller.items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
                .onDelete(perform: deleteItems)
            }
#if os(macOS)
            .navigationSplitViewColumnWidth(min: 180, ideal: 200)
#endif
            .toolbar {
#if os(iOS)
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
#endif
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
        .task {
            await controller.updates(in: modelContext.container)
        }

    }

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

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(controller.items[index])
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}