1

I am trying to use Core Data with MVVM in SwiftUI, but I can't manage to make the List in my view update whenever there are changes to Core Data.

Please check this example. Why my List doesn't update if items is a Published variable?

QUESTION: How to make the List update every time I add or delete items?

Right now I need to force quit the app and open it again to see changes. I've searched for many similar questions and I wasn't able to make it work. Maybe I'm missing something obvious.

EXAMPLE CODE:

ContentView.swift

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @StateObject var vm = ContentViewModel()

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.items) { item in
                    Text(item.text!)
                }
            }
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
                ToolbarItem(placement: .destructiveAction){
                    Button(action: deleteAll) {
                        Image(systemName: "trash")
                            .foregroundColor(.red)
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.text = "List item"

            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)")
            }
        }
    }
    
    private func deleteAll() {
        vm.items.forEach(viewContext.delete)
        
        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)")
        }
    }
}

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

ContentViewModel.swift

import Foundation
import CoreData

final class ContentViewModel: ObservableObject {
    // Why my List doesn't update if items is a Published variable?
    @Published var items: [Item] = []
    let container: NSPersistentContainer = PersistenceController.shared.container
    
    init() {
        let fetchRequest = NSFetchRequest<Item>(entityName: "Item")
        
        do {
            items = try container.viewContext.fetch(fetchRequest)
        } catch let error {
            print("Error fetching. \(error)")
        }
    }
}

Persistence.swift (it's almost the same as default Core Data example file in Xcode 14)

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.text = "Abc"
        }
        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: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CoreDataMVVM")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // 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.

                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

xcdatamodeld has 1 entity: Item with 1 attribute: text: String

mallow
  • 2,368
  • 2
  • 22
  • 63
  • You aren’t observing the store. Use “@FetchRequest” at least until you understand how it all works. This isn’t a simple setup. Also every observable object/core data object should be wrapped in “@ObservedObject” – lorem ipsum Oct 15 '22 at 12:19
  • Thank you for your comment. I know how to make it work with @FetchRequest, but unfortunately I can't make it work in one project and that's why I need to try another way. – mallow Oct 15 '22 at 12:39
  • Hard but best way, listen to core data notifications and update your published property accordingly, easy way that isn’t the most efficient, add a reload function to your view model that will execute the fetch request again and replace the content of the published property and then call this function from your view when necessary. – Joakim Danielson Oct 15 '22 at 12:49
  • Look at the sample project that apple provides with a new project the fetchrequest goes in the view – lorem ipsum Oct 15 '22 at 13:01
  • @loremipsum Thank you, but as I said - I know how to update list using @ FetchRequest. And my example is made based on example project from Apple. But I've tried to rewrite it using MVVM – mallow Oct 15 '22 at 13:14
  • Then you need to listen to the store. NSFetchRequest is a one time pull of data. There is no listening for changes. If you Google the “Core Data Programming guide” it is an old Apple document you’ll find all you need. – lorem ipsum Oct 15 '22 at 13:16
  • Thank you @JoakimDanielson How to listen to core data notification? Using NotificationCenter? – mallow Oct 15 '22 at 13:18
  • 1
    Here is one [article](https://www.donnywals.com/responding-to-changes-in-a-managed-object-context/) on the subject – Joakim Danielson Oct 15 '22 at 16:23
  • In your `func addItem`, and in your `static preview`, you create once or several times `newItem` objects, and set their `text` property. But apparently, you never add them to the `items` property of you `ContentViewModel`, or do you? – Reinhard Männer Oct 20 '22 at 14:38

1 Answers1

0

We don't need MVVM in SwiftUI because the View struct is a view model. If you use a view model object instead, you'll have this consistency problem. To make the list update, use @FetchRequest in the View struct.

FYI FetchRequest is a DynamicProperty struct and gets the Environment's managedObjectContext in its update func.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • Yes, thank you. I know about this and use it often. Thank you for your answer. But I am trying an alternative way with CD and MVVM. – mallow Oct 17 '22 at 05:09