0

I am new to SwiftUI and one thing I am struggling to understand is how do we call CoreData in ObservableObject?

I have the following code in place.

SimpleTodoModel.xcdatamodeld Inside there is a simple entities name: Task

Main Application

import SwiftUI

@main
struct NewApp: App {
    
    let persistentContainer = CoreDataManager.shared.persistentContainer
    
    var body: some Scene {
        WindowGroup {
            ContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
        }
    }
}

CoreDataManager and ContentView

import Foundation
import CoreData

class CoreDataManager {
    
    let persistentContainer: NSPersistentContainer
    static let shared: CoreDataManager = CoreDataManager()
    
    private init() {
        
        persistentContainer = NSPersistentContainer(name: "SimpleTodoModel")
        persistentContainer.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Unable to initialize Core Data \(error)")
            }
        }
        
    }
    
}

struct ContentView: View {
    @EnvironmentObject var obs : observer
    @State private var title: String = ""
    @State private var selectedPriority: Priority = .medium
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key: "dateCreated", ascending: false)]) private var allTasks: FetchedResults<Task>
    
    private func saveTask() {
        
        do {
            let task = Task(context: viewContext)
            task.title = title
            task.priority = selectedPriority.rawValue
            task.dateCreated = Date()
            try viewContext.save()
        } catch {
            print(error.localizedDescription)
        }
        
    }

    var body: some View {
        NavigationView {
            VStack {
                   //making a view and calling coredata values which is working perfectly 
            }
        }
    }
}

Here is where i struggle, calling coredata outside of the view in a class/ObservableObject

class observer : ObservableObject{


     //How should i call the coredata instead? 
     //Say i want to change a piece of value in what i have saved above inside one of the Task record?


}
Ryan Fung
  • 2,069
  • 9
  • 38
  • 60

1 Answers1

1

Presuming you want to do this with a strict MVVM architecture, here is a demonstration of how it may work for you. You will need a data model, and a view model.

You data model essentially mimics the Core Data model:

struct TaskModel: Identifiable {
    
    private var task: Task
    
    init(task: Task) {
        self.task = task
    }
    var id: NSManagedObjectID { // Identifiable conformance
        task.objectID
    }
    var dateCreated: Date {
        task.dateCreated
    }
    var priority: Priority {
        task.priority
    }
    var title: String {
        task.title
    }
}

extension TaskModel {
    init?(taskID: NSManagedObjectID, context: NSManagedObjectContext) {
        do {
            guard let task = try context.existingObject(with: taskID) as? Task else { return nil }
            self.task = task
        } catch {
            return nil
        }
    }
}

Your view model would then look something like this:

@MainActor
class TaskListViewModel: NSObject, ObservableObject {
    
    @Published var tasks: [TaskModel] = []

    private let fetchResultsController: NSFetchedResultsController<Task>
    private let context: NSManagedObjectContext
    
    init(_ compoundPredicate: NSCompoundPredicate, category: CategoryModel? = nil, showTasks: ShowTasks, context: NSManagedObjectContext) {
        self.context = context
        let fetchRequest = Task.fetchRequest()
        
        super.init() // NSObject conformance to use the delegate
        fetchResultsController.delegate = self
        
        do {
            try fetchResultsController.performFetch()
            guard let tasks = fetchResultsController.fetchedObjects else { return }
            
            self.tasks = tasks.map(TaskModel.init)
        } catch {

        }
    }
    
    func deleteTask(_ task: TaskModel) {
        do {
            guard let taskObject = try context.existingObject(with: task.id) as? Task else { return }
            try taskObject.delete()
        } catch {

        }
    }

    func save(_ task: TaskModel) {
        do {
            guard let taskObject = try context.existingObject(with: task.id) as? Task else { return }
            try taskObject.save()
        } catch {

        }
    }    
}

The delegate will cause the view model to update when the Core Data model changes:

extension TaskListViewModel: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let tasks = controller.fetchedObjects as? [Task] else { return }
        self.tasks = tasks.map(TaskModel.init)
    }
}

In your view, I would initialize it as a @StateObject, unless this is a common view model, in which case the first initialization should be as a @StateObject, and then injected appropriately, something like this:

@StateObject private var vm: TaskListViewModel
    
init(vm: TaskListViewModel) {
    _vm = StateObject(wrappedValue: vm)
}

This code has not been tested, so there may be some syntax errors...

Yrb
  • 8,103
  • 2
  • 14
  • 44