0

I'm using CoreData for storage. After following this article and some other additional documentation, I can't seem to overcome NSPersistentContainer returning nil.

import CoreData

public class ContactStore {
    
    private lazy var persistentContainer: NSPersistentContainer = {
        
        // !! The problem will be here
        let persistentContainer = NSPersistentContainer(
            name: "PersistentContact"
        )
        
        // Load the persistent store
        persistentContainer.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Couldn't load") // TODO
            }
        }
        
        return persistentContainer
    }()
    
    private lazy var entityDescription: NSEntityDescription? = {
        
        let entityDescription: NSEntityDescription? = NSEntityDescription.entity(
            forEntityName: "PersistentContact",
            in: self.persistentContainer.viewContext
        )
        
        return entityDescription
    }()
    
    public init() {
        
    }
    
    static var id: Int64 = 0
    
    private func idAndIncrement() -> Int64 {
        
        let rv:Int64 = ContactStore.id
        
        // TODO: use persistence
        ContactStore.id += 1
        
        return rv
    }
    
    public func push(contact: Contact) -> NSManagedObject {
        
        let persistentContact = NSManagedObject(
            entity: self.entityDescription!,
            insertInto: self.persistentContainer.viewContext
        )

        // contact.id is not used            
        persistentContact.setValue(contact.distance, forKey: "distance")
        persistentContact.setValue(contact.duration, forKey: "duration")
        
        do {
            try self.persistentContainer.viewContext.save()
        } catch {
            fatalError("Something here")
            // TODO: failed saving
        }
        
        return persistentContact
    }
    
    public func pop() -> Contact? {
        return nil // TODO
    }
}

Then I test it:

class ContactStoreTests: XCTestCase {
    
    func testCreation() {
        
        // Populate with random values
        let contact = Contact(
            id: "83A8E3C1-3C95-4F0F-9F50-D58E4E1F66F4",
            duration: 8.2,
            distance: 1,
        )
        
        let contactStore: ContactStore = ContactStore()

        contactStore.push(contact: contact)
    }
}

When I run the tests everything builds fine, but I get the error:

+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'PersistentContact' (NSInvalidArgumentException)

If I stop the test with a breakpoint after persistentContainer is initialized, this is what I get:

enter image description here

So it is, in fact, nil. What am I missing?

André Fratelli
  • 5,920
  • 7
  • 46
  • 87
  • 1
    The name in `NSPersistentContainer(name:"PersistentContact")` should be the name of your store, not of the entity. Also, I think `let persistentContact = NSManagedObject` {} should be `let persistentContact = Contact {}` – koen Dec 21 '20 at 18:26
  • I tried `PersistentContact.xcdatamodeld` and got the same result. – André Fratelli Dec 21 '20 at 18:37
  • Notice that `Contact` is a container class, not a model. Basically I work with `Contact` and use `ContactStore` to bridge between my implementation and a model. I can try `PersistentContact`, which is the actual model. – André Fratelli Dec 21 '20 at 18:38
  • It's also notable that the project is an SDK, not an app. I thought that maybe the tests target wasn't copying the store file, but it doesn't make sense because that's already done by the SDK. Still, I tried adding to `Copy Bundle Resources` under the Tests target and didn't work. – André Fratelli Dec 21 '20 at 18:41
  • It should be the name of your sqlite file. So, for instance if your sqlite file is called "MyContactsApp.sqlite", then you need to use: `NSPersistentContainer(name:"MyContactsApp")` – koen Dec 21 '20 at 18:45
  • I only have a `PersistentContact.xcdatamodeld` file; I'm under the impression that Xcode handles the rest. Should there be a SQLite file somewhere in the project? – André Fratelli Dec 21 '20 at 19:09
  • "I can try PersistentContact, which is the actual model." - it should be whatever name for the entity you use in your `xcdatamodeld `. – koen Dec 21 '20 at 19:13
  • "Should there be a SQLite file somewhere in the project?" - yes, but you are correct, that is autogenerated. I meant, it should be the same as your `.xcdatamodel` name (without the extension). Hope this is clear. – koen Dec 21 '20 at 19:15
  • It is, and all of those settings match what I have – André Fratelli Dec 21 '20 at 19:24
  • Is it possible for you to test the code from an app instead of a unit test? – Joakim Danielson Dec 21 '20 at 19:54
  • I'm trying, but getting `Could not attach to pid`. I'm now looking into that. – André Fratelli Dec 21 '20 at 21:51
  • I see [here](https://medium.com/i-am-become-coder-creator-of-code-things/testing-core-data-models-in-swift-with-xctest-da2164e50182) that this guy used an in-memory managed object context. It does seem like one would need some additional steps to run this without an app. But I don't know how this applies to my code. – André Fratelli Dec 21 '20 at 21:56
  • 1
    It's hard to figure out how `persistentContainer` could be nil after initialization, since it's not an optional. Swift shouldn't allow a non-optional attribute to be nil after initialization, ever. – Tom Harrington Dec 21 '20 at 23:08
  • Agreed. And swift goes all the way to the `NSEntityDescription.entity` call without complaining. It only raises on that call. – André Fratelli Dec 22 '20 at 10:14
  • That might just be an issue with the Xcode debugger. I've seen several questions here on SO where the debugger showed nil for a variable that has a value. – Joakim Danielson Dec 22 '20 at 12:49
  • Try setting up your core stack manually, see [here](https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack/setting_up_a_core_data_stack_manually) and see where it goes wrong - maybe it can't find it because your project is a framework. – koen Dec 22 '20 at 12:51
  • 1
    I created a project like yours and managed to reproduce the issue and the underlying problem is that your test target can't find/access the model file. This was discussed in [this question](https://stackoverflow.com/questions/65137756/nspersistentcontainer-will-load-in-app-wont-load-in-test-target/65143033#65143033) but I couldn't make the solution work for a framework the way it works in my app. Maybe the information can push you in the right direction though. – Joakim Danielson Dec 22 '20 at 15:14
  • Thank you! I'll have a look. – André Fratelli Dec 22 '20 at 19:18

1 Answers1

0

Replace this

let persistentContainer = NSPersistentContainer(
            name: "PersistentContact"
        )

with

let persistentContainer = NSPersistentContainer(
        name: "NameOfTheDataModel" 
    )

Replace NameOfTheDataModel with the name of your app xcdatamodeld For example, if you have myApp.xcdatamodeld - the name string become myApp

Luca Sfragara
  • 624
  • 4
  • 16