Home > Software design >  How to preview Core Data data inside SwiftUI Previews
How to preview Core Data data inside SwiftUI Previews

Time:04-12

Here is a demo of what I have (kind of a lot of code, but I hope someone can follow it).
I have one entity inside Core Data named Activity with one string field. For that I use this extension to display the data in the Previews:

extension Activity {
    var _name: String {
        name ?? ""
    }
    
    static var example: Activity {
        let controller = DataController(inMemory: true)
        let viewContext = controller.container.viewContext

        let activity = Activity(context: viewContext)
        activity.name = "running"
        
        return activity
    }
}

For setting up Core Data I use a DataController object:

class DataController: ObservableObject {
    let container: NSPersistentCloudKitContainer
    
    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "Model")
        
        if inMemory {
            container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.loadPersistentStores { storeDescription, error in
            if let _ = error {
                fatalError("Fatal error loading store")
            }
        }
    }
    
    static var preview: DataController = {
        let dataController = DataController(inMemory: true)
        let viewContext = dataController.container.viewContext
        
        do {
            try dataController.createSampleData()
        } catch {
            fatalError("Fatal error creating preview")
        }
        
        return dataController
    }()
    
    func createSampleData() throws {
        let viewContext = container.viewContext
        
        for _ in 1...10 {
            let activity = Activity(context: viewContext)
            activity.name = "run"
        }
        
        try viewContext.save()
    }
}

In the app file I do the following setup:

struct TestApp: App {
    @StateObject var dataController: DataController
    @Environment(\.managedObjectContext) var managedObjectContext
    
    init() {
        let dataController = DataController()
        _dataController = StateObject(wrappedValue: dataController)
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, dataController.container.viewContext)
        }
    }
}

In my ContentView I display a list of this string from Core Data, which works correctly:

struct ContentView: View {
    let activities: FetchRequest<Activity>
    
    init() {
        activities = FetchRequest<Activity>(entity: Activity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: false)], predicate: nil)
    }
    
    var body: some View {
        List {
            ForEach(activities.wrappedValue) { activity in
                ActivityView(activity: activity)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var dataController = DataController.preview
    
    static var previews: some View {
        ContentView()
            .environment(\.managedObjectContext, dataController.container.viewContext)
            .environmentObject(dataController)
    }
}

But in my ActivityView where I display the string in a simple text field, previewing doesn't work.

struct ActivityView: View {
    let activity: Activity
    
    init(activity: Activity) {
        self.activity = activity
    }
    
    var body: some View {
        Text(activity._name)
    }
}

struct ActivityView_Previews: PreviewProvider {
    static var previews: some View {
        ActivityView(activity: Activity.example)
    }
}

I can see the string "run" in my list, 10 times, the way it is setup, but in the ActivityView screen I don't see anything displayed in the preview.
Not sure why is that, I hope someone has an idea.

edit:

I also tried this in the preview, but still doesn't work.

struct ActivityView_Previews: PreviewProvider {
    static var dataController = DataController.preview
    
    static var previews: some View {
        ActivityView(activity: Activity(context: dataController.container.viewContext))
            .environment(\.managedObjectContext, dataController.container.viewContext)
    }
}

CodePudding user response:

In SwiftUI we use the View hierarchy to convert from the rich model types to simple types. So the best way to solve this is to redesign ActivityView to work with simple types rather than the model type then it would be previewable without creating a managed object. I recommend watching Structure your app for SwiftUI previews which covers this technique and offers a few others like protocols and generics.

Btw I also noticed this problem:

init() {
    let dataController = DataController()
    _dataController = StateObject(wrappedValue: dataController)
}

StateObject init uses @autoclosure, e.g.

@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

This means the object init needs to be inside the brackets, e.g.

_dataController = StateObject(wrappedValue: DataController())

This is what prevents the object from being init over and over again every time SwiftUI recalculates the View hierarchy.

  • Related