Search code examples
iosswiftcore-datawidgetkitios-app-group

App Group not working to fetch Core Data with WidgetKit


I'm trying to access my shared Core Data store from WidgetKit. I created an App Group and added both targets to it. In my PersistenceController, I started with just the default code that comes with a Core Data project, but updated it to use my App Group url as the storage path.

Here's my PersistenceController:

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentContainer

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        let samplePokemon = Pokemon(context: viewContext)
        samplePokemon.id = 0
        samplePokemon.name = "bulbasaur"
        samplePokemon.types = ["grass", "poison"]
        samplePokemon.hp = 45
        samplePokemon.attack = 49
        samplePokemon.defense = 49
        samplePokemon.specialAttack = 65
        samplePokemon.specialDefense = 65
        samplePokemon.speed = 45
        samplePokemon.sprite = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png")
        samplePokemon.shiny = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png")
        samplePokemon.favorite = false
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            print(nsError)
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    init(inMemory: Bool = false) {
        let sharedStoreURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.MyAppGroup")!.appending(path: "Poke.sqlite")
        let description = NSPersistentStoreDescription(url: sharedStoreURL)
        
        container = NSPersistentContainer(name: "Poke")
        if inMemory {
            container.persistentStoreDescriptions = [description]
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

This works just fine running in my main app, and the data gets stored in the database just fine too, but the problem I run into is when I try to fetch data for my widget.

Here's the code that deals with Core Data in my widget Provider:

var randomPokemon: Pokemon {
    let context = PersistenceController.shared.container.viewContext
    
    let fetchRequest: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()

    var results: [Pokemon] = []

    do {
        results = try context.fetch(fetchRequest)
    } catch {
        print("Couldn't fetch: \(error)")
    }

    print(results)

    if let randomPokemon = results.randomElement() {
        return randomPokemon
    }

    return SamplePokemon.samplePokemon
}

It always returns samplePokemon, because the results are always empty.

Here's my console log:

2022-08-06 20:55:49.029944-0600 RandomFavoriteExtension[46125:627790] [error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Pokemon' so +entity is unable to disambiguate.
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Pokemon' so +entity is unable to disambiguate.
2022-08-06 20:55:49.030043-0600 RandomFavoriteExtension[46125:627790] [error] warning:       'Pokemon' (0x600002f280b0) from NSManagedObjectModel (0x600003b388c0) claims 'Pokemon'.
CoreData: warning:       'Pokemon' (0x600002f280b0) from NSManagedObjectModel (0x600003b388c0) claims 'Pokemon'.
2022-08-06 20:55:49.030092-0600 RandomFavoriteExtension[46125:627790] [error] warning:       'Pokemon' (0x600002f38160) from NSManagedObjectModel (0x600003b3ccd0) claims 'Pokemon'.
CoreData: warning:       'Pokemon' (0x600002f38160) from NSManagedObjectModel (0x600003b3ccd0) claims 'Pokemon'.
2022-08-06 20:55:49.030185-0600 RandomFavoriteExtension[46125:627790] [error] error: +[Pokemon entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[Pokemon entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
Error Domain=NSCocoaErrorDomain Code=133021 "(null)" UserInfo={NSExceptionOmitCallstacks=true, conflictList=(
    "NSConstraintConflict (0x600000d2f680) for constraint (\n    id\n): database: 0x9147aa890f321eb4 <x-coredata://E5FB4658-9345-4245-B471-FAEE5B98F707/Pokemon/p1>, conflictedObjects: (\n    \"0x9147aa890d921eb4 <x-coredata://E5FB4658-9345-4245-B471-FAEE5B98F707/Pokemon/p20>\"\n)"
)}

I have been working on this for weeks now. Any help would be very appreciated.


Solution

  • I figured this out, so I'll put my solution here so the question isn't just left hanging. The problem I was having was trying to use the shared app group while inMemory was true vs false.

    My working PersistenceController code is here:

    import CoreData
    
    struct PersistenceController {
        static let shared = PersistenceController()
        let container: NSPersistentContainer
    
        static var preview: PersistenceController = {
            let result = PersistenceController(inMemory: true)
            let viewContext = result.container.viewContext
            let samplePokemon = Pokemon(context: viewContext)
            samplePokemon.id = 1
            samplePokemon.name = "bulbasaur"
            samplePokemon.types = ["grass", "poison"]
            samplePokemon.hp = 45
            samplePokemon.attack = 49
            samplePokemon.defense = 49
            samplePokemon.specialAttack = 65
            samplePokemon.specialDefense = 65
            samplePokemon.speed = 45
            samplePokemon.sprite = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png")
            samplePokemon.shiny = URL(string: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png")
            samplePokemon.favorite = false
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                print(nsError)
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
            return result
        }()
    
        init(inMemory: Bool = false) {
            let sharedStoreURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.MyAppGroup")!.appending(path: "Poke.sqlite")
    
            container = NSPersistentContainer(name: "Poke")
            
            if inMemory {
                container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            } else {
                container.persistentStoreDescriptions.first!.url = sharedStoreURL
            }
            
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            container.viewContext.automaticallyMergesChangesFromParent = true
        }
    }
    

    The part to take note of is the if else block inside the init:

    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    } else {
        container.persistentStoreDescriptions.first!.url = sharedStoreURL
    }
    

    My var randomPokemon code from my question is still exactly the same. It didn't have anything to do with the error.