Search code examples
core-dataswiftuisave

SwiftUI 2.0 / CoreData 2021: No error but entity not saved


I have a SwiftUI 2.0 app that uses CoreData to store data locally, and although the preview mode can write data in memory or not (I tested both) when I try to save data to CoreData in the edition View, I get no error message but the data is not written to the sqlite data file for the simulator ... What am I doing wrong?

I have 2 entities in a one-to-many relationship:

Post:

  • date: Date
  • id: UUID
  • text: String
  • timestamp: Date
  • pics: toMany relationship / destination PostPic / inverse post

PostPic:

  • data: BinaryData / Allow External Storage
  • post: toOne relationship / destination Post / inverse pics

I have extracted the main files below:

MyApp.swift

import SwiftUI

@main
struct MyApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            MyNavigationView().environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

Persistence.swift

import Foundation
import SwiftUI
import CoreData
import CloudKit

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for day in 1...10 {
            let newPost = Post(context: viewContext)
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy/MM/dd"
            newPost.date = dateFormatter.date(from: "2021/02/\(String(format: "%02d", day))")!
            newPost.id = UUID()
            newPost.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
            newpost.timestamp = Date()
            for index in 1...7 {
                let newPostPic = PostPic(context: viewContext)
                if let image = UIImage(named:newPostPic.filename!) {
                    newPostPic.data = image.pngData()
                }
                newPost.addToFiles(newPostPic)
            }
        }
        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
    }()

    let container: NSPersistentCloudKitContainer
    private static var _model: NSManagedObjectModel?
    private static func model(name: String) throws -> NSManagedObjectModel {
        if _model == nil {
            _model = try loadModel(name: name, bundle: Bundle.main)
        }
        return _model!
    }
    private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
        guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
            throw CoreDataError.modelURLNotFound(forResourceName: name)
        }

        guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
            throw CoreDataError.modelLoadingFailed(forURL: modelURL)
       }
        return model
    }

    enum CoreDataError: Error {
        case modelURLNotFound(forResourceName: String)
        case modelLoadingFailed(forURL: URL)
    }

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

MyNavigationView.swift

struct MyNavigationView: View {
    @Environment(\.managedObjectContext) private var viewContext
    var body: some View {
        NavigationView {
            TabView { // tab bar at the bottom of the screen
                HomeView()
                    .tabItem{
                        Image(systemName: "house.fill")
                    }.tag(0)
            }
            .navigationBarTitleDisplayMode(.inline)
            .navigationTitle("MyApp")
        }
    }
}

struct MyNavigationView_Previews: PreviewProvider {
    static var previews: some View {
        MyNavigationView()
    }
}

HomeView.swift

//
//  HomeView.swift
//  curum
//
//  Created by loic on 2021/02/19.
//

import SwiftUI

struct HomeView: View {
    var body: some View {
        ZStack {
            // ...
            VStack { // the plus button
                Spacer()
                HStack {
                    Spacer()
                    NavigationLink(destination: EditionView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)) {
                        ZStack {
                            Circle()
                                .frame(width: 80, height: 80)
                            Image(systemName: "plus")
                                .renderingMode(.template)
                                .font(.system(size: 60, weight: .light))
                        }
                    }
                    .padding(.trailing, 16)
                    .padding(.bottom, 16)
                }
            }
        }

    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

EditionView.swift

import Foundation
import SwiftUI
import CoreData

struct EditionView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.presentationMode) var mode: Binding<PresentationMode>
    @State private var postText: String = ""
    @State private var postImage: Image = Image("image1")
    @State private var showingImagePicker = false
    @State private var inputImage: UIImage?
    
    var body: some View {
        VStack {
                TextEditor(text: $postText)
                    .lineSpacing(10)
                    .border(Color.gray)
                    .padding(.all)
            HStack {
                Button(action: {
                    self.showingImagePicker = true
                }) {
                    
                    if (inputImage == nil) {
                        Text("Pick Image")
                    } else {
                        postImage
                            .resizable()
                            .renderingMode(.original)
                            .frame(width: 100, height: 100)
                    }
                }.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
                    ImagePicker(image: self.$inputImage)
                }
            }
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: Button(action : {
            self.mode.wrappedValue.dismiss()
        }){
            Image(systemName: "xmark")
        },
        trailing: Button {
                let newPost = Post(use: viewContext)
                newPost.date = Date()
                newPost.id = UUID()
                newPost.text = entryText
                newPost.timestamp = Date()
                let newPostPic = PostPic(use: viewContext)
                newPostPic.data = inputImage!.pngData()
                newPost.addToFiles(newPostPic)
                do {
                    try viewContext.save()
                    mode.wrappedValue.dismiss()
                } catch {
                    let nsError = error as NSError
                    fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                }
            } label: {
                Image(systemName: "checkmark.circle.fill")
            })
    }
    func loadImage() {
        guard let inputImage = inputImage else { return }
        postImage = Image(uiImage: inputImage)
    }
}

struct EditionView_Previews: PreviewProvider {
    static var previews: some View {
        EditionView()
    }
}

struct ImagePicker: UIViewControllerRepresentable {
    @Environment(\.presentationMode) var presentationMode
    @Binding var image: UIImage?

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: ImagePicker

        init(_ parent: ImagePicker) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                parent.image = uiImage
            }

            parent.presentationMode.wrappedValue.dismiss()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }
}

** Edit in response to @Tushar Sharma** I forgot to add that I added the following extension to NSManagedObject to get rid of an error like in Multiple NSEntityDescriptions Claim NSManagedObject Subclass:

import Foundation
import CoreData

public extension NSManagedObject {
    convenience init(use context: NSManagedObjectContext) {
        let name = String(describing: type(of: self))
        let entity = NSEntityDescription.entity(forEntityName: name, in: context)!
        self.init(entity: entity, insertInto: context)
    }
}

Solution

  • Sorry, it was a simple copy-paste problem in the HomeView.swift file:

    On the following line, I used the preview persistence context ...

    NavigationLink(destination: EditionView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)) {
    
    

    ... instead of using the shared one:

    NavigationLink(destination: EditionView().environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)) {
    
    

    That solved my problem!