Search code examples
swiftsirishortcutsappintents

Core Data fetch in AppIntent is crashing when app is running in background


I’m building an AppIntent Shortcut and want to perform fetch requests of my apps Core Data to feed the Shortcut with the data.

My Code

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
    }
}

Problem

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?


Solution

  • 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
        }
    
        // …
    }