/dev/trouble
Eric Roller's Development Blog

SwiftUI Preview of a Core Data Model Object

- Posted in iOS by

SwiftUI would be impossible without the preview canvas where our view model can be inspected live. When using Core Data, however, I don't want to preview actual data; I want to be able to supply custom preview-specific data.

To do this, I am creating an in-memory persistent store with hard-coded data:

import SwiftUI
import ShopCore    // Private framework
import CoreData

// …

#if DEBUG

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        return ContentView(store: previewStore)
    }

    static let previewStore: ShopKeeper = {
        // Create a dedicated instance, without loading from Core Data
        let store = ShopKeeper()

        // Create the preview coordinator
        let context = PreviewCoordinator.shared.viewContext

        let previewShop =
            NSEntityDescription.insertNewObject(forEntityName:
            ShopModel.entityName(), into: context)
            as! ShopModel

        // Data to be shown in the preview canvas:
        previewShop.name = "Preview Shop"
        previewShop.comment = """
This is a dynamically created preview shop that
is stored in an in-memory \"persistent\" store.
"""
        // …

        try? context.save()

        // Show just our preview store
        store.list = [ previewStore ]
        return store
    }()
}

And this is how the in-memory persistent store is set up for the preview:

private class PreviewCoordinator {
    static let shared = PreviewCoordinator()

    let objectModel: NSManagedObjectModel
    let storeCoordinator: NSPersistentStoreCoordinator
    let viewContext: NSManagedObjectContext

    init() {
        objectModel = NSManagedObjectModel()
        objectModel.entities = [
            ShopModel.previewDescription()
        ]

        storeCoordinator = 
            NSPersistentStoreCoordinator(managedObjectModel:
            objectModel)

        viewContext = NSManagedObjectContext(concurrencyType:
            .mainQueueConcurrencyType)

        do {
            try storeCoordinator.addPersistentStore(ofType:
                NSInMemoryStoreType, configurationName: nil, at: nil,
                options: nil)
            viewContext.persistentStoreCoordinator = storeCoordinator
        } catch {
        }
    }
}

#endif

To make this work, our managed object needs to be able supply its entity (class) name:

extension NSManagedObject {
    public class func entityName() -> String {
        // Typically: "MyApp.MyModel"
        let moduleAndClassName = NSStringFromClass(object_getClass(self)!)
        // Return just: "MyModel"
        return String(moduleAndClassName.split(separator: ".").last!)
    }
}

I cannot load the Core Data model description, since it is located in an embedded framework. Therefore, the entity property cannot be used; instead the ShopModel needs to supply a preview-specific entity description:

import CoreData

#if DEBUG

extension ShopModel {
    // Returns the entity description, mirroring what is defined
    // in the .xcdatamodeld file for Core Data.
    // Beware: To be kept in sync with the .xcdatamodeld !!!
    public class func previewDescription() -> NSEntityDescription {
        let entity = NSEntityDescription()

        entity.name = entityName()
        entity.managedObjectClassName =
            "ShopCore." + (entity.name ?? "none")
        entity.renamingIdentifier = entity.name

        entity.properties = [
            NSAttributeDescription(name: "id",
                type: .UUIDAttributeType, optional: true),
            NSAttributeDescription(name: "name",
                defaultValue: "New Shop"),
            // …
        ]
    }
}

#endif

The most critical part turned out to be the managedObjectClassName - it needs to be set correctly or else the as! ShopModel cast will fail. Normally, the class name would be something like "ShopApp.ShopModel", but in my case, where the ShopModel is declared in an embedded "ShopCore" framework, it would be "ShopCore.ShopModel". To eliminate these conflicts, it is a good idea to manually set the Module (to "ShopCore") for each object in the .xcdatamodeld editor.

To streamline the above code, I also added a convenience initialiser, with defaults that are specific to my use case:

#if DEBUG

extension NSAttributeDescription {
    // Used below to create properties for the entity description.
    convenience public init(name: String,
        type: NSAttributeType = .stringAttributeType,
        defaultValue: Any? = nil, optional: Bool = false) {

        self.init()

        self.name = name
        self.renamingIdentifier = name
        self.isOptional = optional
        self.defaultValue = defaultValue
        self.attributeType = type

        switch type {
        case .stringAttributeType:
            self.attributeValueClassName = "NSString"
        case .dateAttributeType:
            self.attributeValueClassName = "NSDate"
        case .UUIDAttributeType:
            self.attributeValueClassName = "NSUUID"
        default:
            self.attributeValueClassName = "NSNumber"
        }
    }
}

#endif

Update History

[2020-08-14] Now using a constant managedObjectClassName prefix. [2020-08-09] Renamed entityDescription() to previewDescription(), clarifying why entity cannot be used. Moved previewStore into the ContentView_Preview struct, avoiding the use of a global valiable. Moved convenience init() to be last.