Best practice for centralizing SwiftData query logic and actions in an @Observable manager?

I'm building a SwiftUI app with SwiftData and want to centralize both query logic and related actions in a manager class. For example, let's say I have a reading app where I need to track the currently reading book across multiple views.

What I want to achieve:

@Observable
class ReadingManager {
    let modelContext: ModelContext
    
    // Ideally, I'd love to do this:
    @Query(filter: #Predicate<Book> { $0.isCurrentlyReading })
    var currentBooks: [Book]  // ❌ But @Query doesn't work here
    
    var currentBook: Book? {
        currentBooks.first
    }
    
    func startReading(_ book: Book) {
        // Stop current book if any
        if let current = currentBook {
            current.isCurrentlyReading = false
        }
        book.isCurrentlyReading = true
        try? modelContext.save()
    }
    
    func stopReading() {
        currentBook?.isCurrentlyReading = false
        try? modelContext.save()
    }
}

// Then use it cleanly in any view:
struct BookRow: View {
    @Environment(ReadingManager.self) var manager
    let book: Book
    
    var body: some View {
        Text(book.title)
        Button("Start Reading") {
            manager.startReading(book)
        }
        if manager.currentBook == book {
            Text("Currently Reading")
        }
    }
}

The problem is @Query only works in SwiftUI views. Without the manager, I'd need to duplicate the same query in every view just to call these common actions.

Is there a recommended pattern for this? Or should I just accept query duplication across views as the intended SwiftUI/SwiftData approach?

Answered by DTS Engineer in 871730022

A SwiftData query (@Query) is currently tied to a SwiftUI view and relies on the modelContext environment value of the view. There is currently no way to use @Query outside of a SwiftUI view.

To implement the query logics in a controller class, you basically need to to do the following with your own code:

  1. Fetch data using FetchDescriptor + ModelContext.fetch.
  2. Update the result set when relevant changes happen.

To do step 2, assuming you are using the SwiftData default store, which is based on Core Data, you need to observe the notifications triggered by a store change (basically NSManagedObjectContextObjectsDidChange and .NSPersistentStoreRemoteChange), check if the change is relevant, and re-fetch the data set as needed.

All these will be a bit involved, and yet have already implemented (as Query) on the framework side. I'd hence suggest that you file a feedback report to request an API that does what Query does but doesn't rely on SwiftUI – If you do so, please share your report ID here for folks to track.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

A SwiftData query (@Query) is currently tied to a SwiftUI view and relies on the modelContext environment value of the view. There is currently no way to use @Query outside of a SwiftUI view.

To implement the query logics in a controller class, you basically need to to do the following with your own code:

  1. Fetch data using FetchDescriptor + ModelContext.fetch.
  2. Update the result set when relevant changes happen.

To do step 2, assuming you are using the SwiftData default store, which is based on Core Data, you need to observe the notifications triggered by a store change (basically NSManagedObjectContextObjectsDidChange and .NSPersistentStoreRemoteChange), check if the change is relevant, and re-fetch the data set as needed.

All these will be a bit involved, and yet have already implemented (as Query) on the framework side. I'd hence suggest that you file a feedback report to request an API that does what Query does but doesn't rely on SwiftUI – If you do so, please share your report ID here for folks to track.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Best practice for centralizing SwiftData query logic and actions in an &#64;Observable manager?
 
 
Q