I’m building an AppIntent Shortcut and want to perform fetch requests of my apps Core Data to feed the Shortcut with the data.
Core Data Handling:
class HomesHandler: NSObject, ObservableObject {
static let shared = HomesHandler()
let objectContext = PersistenceController.shared.container.viewContext
func fetchAllHomes() -> [Home] {
let request: NSFetchRequest<Home> = Home.fetchRequest()
// PERFORM FETCH
do {
return try objectContext.fetch(request)
} catch let error {
print("Error fetching Homes: \(error.localizedDescription)")
return []
}
}
}
AppIntent Code:
struct HomeIntentQuery: EntityPropertyQuery {
// Populates the list when the user taps on a parameter that accepts a Home.
func suggestedEntities() async throws -> [HomeIntentEntity] {
// Fetch All Homes
// Sorted by non-archived first
let allHomes = HomesHandler.shared.fetchAllHomes().sorted(by: { !$0.isArchived && $1.isArchived })
// Map Core Data to AppEntity
return allHomes.map { home in
HomeIntentEntity(id: home.id,
title: home.wrappedTitle,
isArchived: home.isArchived)
}
}
// …
Intent Entity to Core Data Bridge:
struct HomeIntentEntity: AppEntity, Identifiable {
// Required to conform to Identifiable
var id: UUID
// Properties mirrored from Core Data
@Property(title: "Title") var title: String
@Property(title: "Is Archived") var isArchived: Bool
// …
}
Core Data Persistence Controller:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
// An initializer to load Core Data,
// optionally able to use an in-memory store.
init(inMemory: Bool = false) {
// Access actual Core Data file
container = NSPersistentContainer(name: "MeterStats")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Core Data Loading Error: \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
The problem I have is that the code crashes with a EXC_BREAKPOINT whenever the app is running in the background. Once the app is closed the fetch request just works fine. How can that be?
I’ve already tried to wrap the do try objectContext.fetch(request)
into objectContext.performAndWait
. Though, that leads to a crash inside the .sorted(by:)
when trying to access .isArchived
The strange thing is that this works perfectly fine at this great GitHub example by Alex Hay. He uses the same code to do the fetch request as well as the AppIntent code.
So, how do I have to handle fetch requests that are running inside AppIntents correctly?
The issue was that for some reason the fetch request was performed outside the apps main thread. Not sure why it works in the Booky example without it, though.
To fix this issue, I’m now using the try context.performAndWait {}
action to perform the fetch request inside. Important to note is to do the mapping from the Core Data Entity to the custom AppEntity
inside the performAndWait
! Otherwise it will lead to a crash when creating the AppEntity
object.
struct HomeIntentQuery: EntityPropertyQuery {
// Populates the list when the user taps on a parameter that accepts a Home.
func suggestedEntities() async throws -> [HomeIntentEntity] {
// Fetch All Homes
// Sorted by non-archived first
let context = PersistenceController.shared.container.viewContext
let request: NSFetchRequest<Home> = Home.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Home.isArchived, ascending: true)]
var allHomes: [HomeIntentEntity] = []
try context.performAndWait {
do {
allHomes = try context.fetch(request).map { home in
// Map Core Data to AppEntity
HomeIntentEntity(id: home.id, title: home.wrappedTitle, isArchived: home.isArchived)
}
} catch {
throw error
}
}
return allHomes
}
// …
}