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.