JohnSF
JohnSF

Reputation: 4300

SwiftUI Preview Does Not Work with Core Data when Entity Injected in View

I've been frustrated for months trying to get previews to work in Xcode when Core Data is used. I can definitely make the preview work for the views that don't include an injected item. But not any subsequent views that depend on the source Entity.

Let's say I have a master/detail-like SwiftUI project with ContentView containing my list and ThingDetailView showing the detail for the entity Thing.

I created a wrapper for the preview of ContentView:

struct PreviewCoreDataWrapper<Content: View>: View {
    @Environment(\.managedObjectContext) private var viewContext
    let content: (NSManagedObjectContext) -> Content

    var body: some View {
        let managedObjectContext = viewContext
    
        let sampleThing = Thing(context: managedObjectContext)
        sampleThing.name = "Sample Name"
        sampleThing.comment = "Sample Comment"
        sampleThing.id = UUID()
        //more attributes

        return self.content(managedObjectContext)
    }

    init(@ViewBuilder content: @escaping (NSManagedObjectContext) -> Content) {
        self.content = content
    }
}

Then the preview in ContentView, this WORKS.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
    
        PreviewCoreDataWrapper { managedObjectContext in
            ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
        }
    }
}

But then in the detail view, no matter what I have tried, I cannot get the preview to work. I've tried dozens of ways to satisfy the injected Thing call.

struct ThingDetailView_Previews: PreviewProvider {
    static var previews: some View {
        PreviewCoreDataWrapper { managedObjectContext in
            ThingDetailView(thing: sampleThing).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
        }
    }
}

Any guidance would be greatly appreciated. Xcode Version 12.0 (12A7208), iOS 14

The ThingDetailView is pretty standard stuff:

struct ThingDetailView: View {

    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) private var managedObjectContext

    var thing: Thing

    @State private var localName: String = ""
    @State private var localComment: String = ""
    //bunch more properties

    var body: some View {
    
        DispatchQueue.main.async {
            self.localName = self.thing.wrappedName
            self.localComment = self.thing.wrappedComment
            //buch more properties
        }//dispatch
        DispatchQueue.main.async {
            if !showEditView {
            localNewUIImage = UIImage(data: thing.tImage)
            }
        }
    
        //you need this to allow the TextEditor background to be changed
        UITextView.appearance().backgroundColor = .clear

        return ScrollView(.vertical, showsIndicators: false) {
            VStack(alignment: .center, spacing: 20) {
            
                //this is for an image
                if !showEditView {
                ThingHeaderView(thing: thing)
                } else {
                    NewPhotoView(myImage: $localMyImage, newUIImage: $localNewUIImage, disableSaveButton: $localDisableSaveButton)
                }
            
                VStack(alignment: .leading, spacing: 10) {
                    Group {//group 1
                        //MARK: name
                        VStack (alignment: .leading) {
                            if !showEditView {
                                Text("\(thing.wrappedName)")
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                    .padding(.vertical, 5)
                                    .textFieldStyle(RoundedBorderTextFieldStyle())
                                    .font(.system(size: 22, weight: .bold, design: .default))
                                    .foregroundColor(Color("CardBlue"))
                            } else {
                                TextField("tf name", text: self.$localName)
                                    .modifier(TextFieldSetup())
                            }
                        }//name v
                    
                        //MARK: comment
                        VStack (alignment: .leading) {
                            if !showEditView {
                                Text("\(thing.wrappedComment)")
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                    .padding(.vertical, 5)
                                    .textFieldStyle(RoundedBorderTextFieldStyle())
                                    .font(.headline)
                            } else {
                                TextEditor(text: self.$localComment)
                                    .modifier(TextEditorSetup())
                            }
                        }//comment v
                        //bunch more TextFields and TextEditors
                    }//group 1
                }//inner v
                .padding(.horizontal, 20)
                .frame(maxWidth: 640, alignment: .center)
            }//outer v
        
            //seems like a title is needed to remofe large space at top - then hide
            .navigationBarTitle(thing.wrappedName, displayMode: .inline)
            //.navigationBarHidden(true)
            .navigationBarBackButtonHidden(showEditView)
            .navigationBarItems(
            
                leading:
                    Button(action: {
                        if showEditView {
                            self.showEditView = false
                        }
                    }) {
                        Text(showEditView ? "Cancel" : "")
                            .font(.system(size: 20))
                    },
                trailing:
                    Button(action: {
                        if !showEditView {
                            print("showEditView is \(showEditView)")
                            self.showEditView = true
                        } else {
                            self.saveEditedRecord()
                            self.showEditView = false
                        }
                    }) {
                        Image(systemName: self.showEditView ? "square.and.arrow.down.fill" : "square.and.pencil")
                            .font(.system(size: 25))
                            .frame(width: 60, height: 60)
                    }//images
                    .disabled(self.localDisableSaveButton)
            )//nav bar item
        }//scroll
        .navigationViewStyle(StackNavigationViewStyle())
    }//body

    func saveEditedRecord() {
        print("...and you are in the save function...")
        let context = self.managedObjectContext
        thing.id = UUID()
        //all the rest and 
        //standard Core Data save
    }//save record
}//struct thing detail view

And the ThingHeaderView:

struct ThingHeaderView: View {

    @Environment(\.horizontalSizeClass) var sizeClass
    @State private var isAnimatingImage: Bool = false

    var thing: Thing

    var body: some View {
    
        let sc = sizeClass == .compact
    
        return ZStack {
        
            if UIImage(data: thing.tImage) != nil {
                Image(uiImage: UIImage(data: thing.tImage)!)
                    .resizable()
                    .renderingMode(.original)
                    .aspectRatio(contentMode: .fit)
                    .cornerRadius(10)
                    .shadow(color: (Color.black.opacity(0.5)), radius: 8, x: 10, y: 10)
                    .padding(sc ? 6 : 10)
            } else {
                Image(systemName: "camera.circle.fill")
                    .resizable()
                    .frame(width: 60, height: 60, alignment: .center)
            }
        }
        .onAppear {
            withAnimation(.easeOut(duration: 0.5)) {
                isAnimatingImage = true
            }
        }
    }
}

Upvotes: 2

Views: 1885

Answers (1)

Hunter Meyer
Hunter Meyer

Reputation: 314

The way I've always handled Core Data in previews is not by using a "PreviewCoreDataWrapper", but to instead define my viewContext and add the properties in the previews before I return my view to preview.

struct ThingDetailView_Previews: PreviewProvider {

    static var viewContext = PersistenceController.preview.container.viewContext

    static var previews: some View {
        let sampleThing = Thing(context: viewContext)
        sampleThing.name = "Sample Name"
        sampleThing.comment = "Sample Comment"
        sampleThing.id = UUID()
        //more attributes

        return ThingDetailView(thing: sampleThing).environment(\.managedObjectContext, viewContext)
    }
}

This usually works for me. Good luck!

Upvotes: 4

Related Questions