/dev/trouble
Eric Roller's Development Blog

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.

For a long time, I have been trying to avoid scroll bars in the screenshots that I generate. The solution that I came up with was simply to wait for them to disappear:

thirdBlock.element.swipeUp()

// Wait for the scroll indicators to be hidden
Thread.sleep(forTimeInterval: 2.5)

takeSnapshot("Last_Block")

I have also been using fastlane for several years to automate this. While I try very hard never to add any screenshot-specific exceptions to the code, there has always been the odd need to do it, for instance to handle situations that are not supported by the iOS simulator.

Preprocessor Macros

For code sections that are only used during development, you should mask them out using "preprocessor macros" like:

#if DEBUG
print("Data finished loading.")
#endif

This works using an Xcode project setting that defines SWIFT_ACTIVE_COMPILATION_CONDITIONS to use DEBUG, which results in the command-line option -D DEBUG to be used by CompileSwift. You can see this if you dive deep into your build logs.

Similarly, you can mask sections that are only (or never) to be used in the simulator like this:

#if targetEnvironment(simulator)
    // iOS simulator only
#else
    // iOS device only
#endif

Simple inversions are also supported in Swift using an exclamation mark (read it as: "not"):

#if ! targetEnvironment(simulator)
    // iOS device only
#endif

Fastlane-Specific Code

The same can be done for fastlane-specific code, but it is necessary to define a macro, for instance in the fastlane/Snapfile. Here we define FASTLANE:

# Add a define for xcodebuild
xcargs "SWIFT_ACTIVE_COMPILATION_CONDITIONS=FASTLANE"

which allow us to use in the code:

#if FASTLANE
    // fastlane only
#endif

No Scroll Bars in Fastlane

Using the above macro, I have now simply disabled the scroll bar indicators in the code:

override func viewDidLoad() {
    super.viewDidLoad()

    navigationItem.rightBarButtonItem = editButtonItem

    #if FASTLANE
    tableView.showsVerticalScrollIndicator = false
    tableView.bounces = false
    #endif
}

And this means that the test script can be made to run without additional delays (compare above):

thirdBlock.element.swipeUp()
takeSnapshot("Last_Block")