Victor Sea
Victor Sea

Reputation: 431

SwiftUI Preview canvas and Core Data

Preview canvas is is crashing but in simulator everything working fine. I assuming it related to @ObservedObject and @Fetchrequest...

tried solution for here Previewing ContentView with CoreData

doesn't work

 import SwiftUI
 import CoreData

struct TemplateEditor: View {

@Environment(\.managedObjectContext) var managedObjectContext

@FetchRequest(
    entity: GlobalPlaceholders.entity(),
    sortDescriptors: [
        NSSortDescriptor(keyPath: \GlobalPlaceholders.category, ascending: false),
    ]
) var placeholders: FetchedResults<GlobalPlaceholders>


@ObservedObject var documentTemplate: Templates
@State private var documentTemplateDraft = DocumentTemplateDraft()
@Binding var editing: Bool


var body: some View {

    VStack(){
        HStack(){
            cancelButton
            Spacer()
            saveButton
        }.padding()
        addButton
        ForEach(placeholders)  {placeholder in
            Text(placeholder.name)
        }
        TextField("Title", text: $documentTemplateDraft.title)

        TextField("Body", text: $documentTemplateDraft.body)
            .padding()
            .frame(width: 100, height:400)
        Spacer()
    }

...




}
struct TemplateEditor_Previews: PreviewProvider {
    static var previews: some View {

    let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Templates")
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Templates.created, ascending: false)]
    let documentTemplate = try! managedObjectContext.fetch(request).first as! Templates


    return TemplateEditor(documentTemplate: documentTemplate, editing: .constant(true)).environment(\.managedObjectContext, managedObjectContext).environmentObject(documentTemplate)

    }
}

Expected to generate preview

Upvotes: 30

Views: 20958

Answers (10)

Azure
Azure

Reputation: 257

What works for me:

I create all of my sample data in the preview property of my persistence controller, building off of the template generated by Xcode when starting a project with the following settings: Interface - SwiftUI, Lifecycle - SwiftUI App, Use Core Data, Host in CloudKit. I have posted the template here:

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext

        // ** Prepare all sample data for previews here ** //

        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            // handle error for production
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "SwiftUISwiftAppCoreDataCloudKit")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // handle error for production
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

In my preview, I inject the persistence controller into the preview environment and for my view argument I use the registeredObjects.first(where:) method on the preview viewContext to pull the first object of the desired type:

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(item: PersistenceController.preview.container.viewContext.registeredObjects.first(where: { $0 is Item }) as! Item)
            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

Edited 11/15/21

Persistence

    import CoreData
    
    struct PersistenceController {

        static let shared = PersistenceController()
    
        static var preview: PersistenceController = {
            let result = PersistenceController(inMemory: true)
            let viewContext = result.container.viewContext
            Seed().prepareData(for: viewContext)
            return result
        }()
    
        let container: NSPersistentCloudKitContainer
    
        init(inMemory: Bool = false) {
            container = NSPersistentCloudKitContainer(name: "SwiftUISwiftAppCoreDataCloudKit")
            if inMemory {
                container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            }
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    // handle error for production
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
        }
    }

    struct Seed {
        func prepareData(for viewContext: NSManagedObjectContext) {
            // ** Prepare all sample data for previews here ** //

            for _ in 0..<10 {
                let newItem = Item(context: viewContext)
                newItem.timestamp = Date()
            }
            do {
                try viewContext.save()
            } catch {
                // handle error for production
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

Item Preview

    struct ItemView_Previews: PreviewProvider {
    
        static let persistence = PersistenceController.preview
    
        static var item: Item = {
            let context = persistence.container.viewContext
            let item = Item(context: context)
            item.timestamp = Date()
            return item
        }()
        
        static var previews: some View {
            ItemView(item: item)
                .environment(\.managedObjectContext, persistence.container.viewContext)
        }
    }

Upvotes: 13

Sam
Sam

Reputation: 197

The thing is you need to find out which line cause the crash.

Since the canvas doesn't show the detailed error, using OSLog and Console.app to debug would be a possible solution.

For example:

import os.log

struct YourView_Previews: PreviewProvider {
    static var previews: some View {
        os_log("[DEBUG]-\(#function)---1--")

        let moc = PersistenceController.preview.container.viewContext

        os_log("[DEBUG]-\(#function)---2--")

        let item = Item.previewData(context: moc)

        os_log("[DEBUG]-\(#function)---3--")

        return YourView(item: item, now: Date())
            .environment(\.managedObjectContext, moc)
    }
}

Remember to use filter to better catch the debug message from Console.

After finding out which line cause the crash, you can further look into the line and continue the process until you find the culprit.

(In my case, I forgot to add UUID to the preview data which causing the canvas to crash.)

Upvotes: 0

Niklas
Niklas

Reputation: 1331

This worked for me. In the AppDelegate create a different preview context and fill it with objects.

    lazy var persistentContainerPreview: NSPersistentContainer = {
    let persistentContainer = NSPersistentContainer(name: "MyModel")
    
    persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {

        }
    })

    let didCreateSampleData = UserDefaults.standard.bool(forKey: "didCreateSampleData")
    if !didCreateSampleData {
        let context = persistentContainer.viewContext
        let recipe = Recipe(context: context)
        recipe.title = "Soup 2"
        recipe.difficultyName = "NOT TOO TRICKY"
        recipe.difficultyValue = 1
        recipe.heroImage = "dsfsdf"
        recipe.ingredients = "meat"
        recipe.method = "sdcsdsd"
        recipe.published = Date()
        recipe.recipeId = 1
        recipe.servings = 4
        recipe.tags = "sdfs"
        recipe.totalTime = 100
        recipe.totalTimeFormatted = "Less than 2 hours"
        try! context.save()
    }

    return persistentContainer
}()

Then in your preview.

struct RecipeView_Previews: PreviewProvider {

static var previews: some View {
    
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainerPreview.viewContext
    let recipe = try! context.fetch(Recipe.fetchRequest()).first as! Recipe
    RecipeView(recipe: recipe).environment(\.managedObjectContext, context)
}

}

Upvotes: 1

madpoet
madpoet

Reputation: 1063

It crashes because it was instructed so in the PersistenceController:

struct PersistenceController {
    ...
    static var preview: PersistenceController = {
        ...
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()
    ...
}

So the actual reason can be seen in the crash report. Actually, XCode 12.4 shows a warning about checking the crash report; however, the report is too verbose for a newbie like me from web development. Thus it took me a while to find out the problem, so I hope this would save some time for others.

...and the problem in my case was a required attribute was not set while populating the core data model for previews.

Upvotes: 0

YanSte
YanSte

Reputation: 10839

This is my solution.

I don't want use CoreData in view. I want MVVM style. So you need to mock Core data for display in Canvas view.

This is an example :

// View
struct MyView: View {
    @ObservedObject var viewModel: PreviewViewModel
}

// View Model
final class MyViewModel: ObservableObject {
   @Published var repository: RepositoryProtocol // CoreData
}

// Repository
protocol RepositoryProtocol { }
class Repository: RepositoryProtocol { ... } 
class MockRepository: RepositoryProtocol { ... } // Create a Mock


// Init of your view
// If Canvas use mock
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
    repository = MockRepository() 
    
// else App use Repository
} else {
    repository = Repository.shared
}

let viewModel = MyViewModel(repository:repository)
MyViewModel(viewModel: viewModel)

Upvotes: 1

George
George

Reputation: 30501

Works for SwiftUI 2 app using the App template

I also had the previews crash and none of the other solutions were suitable or worked for me.

What I did was rather than the following:

struct ContentView_Previews: PreviewProvider {
  
    static var previews: some View {
        return ContentView()
            .environment(
                \.managedObjectContext,
                CoreDataManager.context
            )
    }
}

I fixed it with:

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        let context = CoreDataManager.context

        /* Optional sample data can be inserted here */
        
        return ContentView()
            .environment(
                \.managedObjectContext,
                context
            )
    }
}

Where CoreDataManager is:

enum CoreDataManager {
    
    static var context: NSManagedObjectContext {
        persistentContainer.viewContext
    }
    
    static let persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "MyContainerName")
        
        container.loadPersistentStores { description, error in
            guard let error = error else { return }
            fatalError("Core Data error: '\(error.localizedDescription)'.")
        }
        
        return container
    }()
}

Not exactly sure why this helped, but now it works perfectly. Additionally you can add sample data to this context where I have marked with a comment.

Upvotes: 1

Keoni
Keoni

Reputation: 89

One option is to NOT use Core Data in previews. This is helpful enough to see the UI of what I'm building but I'll still need to use Simulator to test the functionality.

#if !DEBUG
// Core Data related code e.g. @FetchRequest
#endif

What was suggested in Previewing ContentView with CoreData worked for me, Xcode Version 11.0 (11A419c) Mac OS 10.15 Beta (19A558d). My crash logs showed an index error,

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray'

because there was no data there, so I had to handle this unique "preview" case and that got things working.

Upvotes: 0

Trapp
Trapp

Reputation: 4349

This answer seems to work in my recent project by replacing the default ContentView_Previews struct, though others are questioning whether it pulls persistent data. Credit goes to @ShadowDES - in the Master/Detail template project in Xcode Beta 7

I'm able to CRUD anything using Canvas (XCode Version 11.3 (11C29)) and it seems to run flawlessly.

    #if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return ContentView().environment(\.managedObjectContext, context)
    }
}
#endif

Add Values, mark priority

Swipe to delete

Deleted!

Upvotes: 13

Don Nissen
Don Nissen

Reputation: 431

I'm not sure if your try line will work if there is no data.

let documentTemplate = try! managedObjectContext.fetch(request).first as! Templates

To get mine to work I created a test Item to use. Like this:

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Test data
        let newEvent = Event.init(context: context)
        newEvent.timestamp = Date()
        return DetailView(event: newEvent).environment(\.managedObjectContext, context)
    }
}

I've also noticed that I needed the .environment(.managedObjectContext, context) code in an earlier tabView that hosted the CoreData views or the preview would fail.

Upvotes: 42

Tim Dorr
Tim Dorr

Reputation: 4931

So, if you put some code in an onAppear handler in the preview, it will run on boot. It even live updates as you type!

struct TemplateEditor_Previews: PreviewProvider {
  static var previews: some View {
    TemplateEditor().environment(\.managedObjectContext, AppDelegate.viewContext).onAppear {
      let entity = GlobalPlaceholders(context: AppDelegate.viewContext)
      entity.name = "abc123"

      // Or create more, if you need more example data

      try! AppDelegate.viewContext.save()
    }
  }
}

Note that I've wrapped up my viewContext in a static method on AppDelegate to make access a tiny bit less verbose and easier to remember:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  static var persistentContainer: NSPersistentContainer {
    return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
  }

  static var viewContext: NSManagedObjectContext {
    return persistentContainer.viewContext
  }

Upvotes: 2

Related Questions