【问题标题】:New Entries only showing on App Reload - CoreData and SwiftUI with MVVM仅在 App Reload 上显示的新条目 - CoreData 和 SwiftUI 与 MVVM
【发布时间】:2021-09-06 17:03:28
【问题描述】:

使用虚拟项目来了解更多关于使用 MVVM 在 SwiftUI 中的 Core Data 的信息。打印语句显示正在保存对象,但除非重新启动应用程序,否则不会显示新条目。已删除的对象会立即删除。所以不知道让新条目立即出现在哪里我错了。我想我可以在每次保存后重新调用 fetchTasks,但由于 fetch 任务在 TaskListViewModel 中,我不知道该怎么做,我应该将 fetch 任务移动到 CoreDataHandler 吗?

参赛作品

import SwiftUI

@main
struct TestCore: App {
    
    //Inject Core Data container managed object context into environment
    let coreDataHandler = CoreDataHandler.shared
    
    //Optional: Add property to monitor scene phase
    @Environment(\.scenePhase) var scenePhase
    
    var body: some Scene {
        WindowGroup {
            //Attach Core Date view context
            TaskListView().environment(\.managedObjectContext, coreDataHandler.container.viewContext)
        }
        //Whenver the app is moved to the background CoreDataUtility save() function is called to save any changes
        .onChange(of: scenePhase) { _ in
            coreDataHandler.save()
        }
    }
}

CoreDataHandler.swift

import Foundation
import CoreData

struct CoreDataHandler {
    //Create a singleton to be used/shared throughout app
    static let shared = CoreDataHandler()
    
    //Create the storage container for Core Data
    let container: NSPersistentContainer
    
    //MARK: - Test Configuration for SwiftUI Previews
    //Create a test configuration for SwiftUI Previews
    static var preview: CoreDataHandler = {
        let handler = CoreDataHandler(inMemory: true)
        
        //Create example task entries
        for _ in 0..<10 {
            let task = Task(context: handler.container.viewContext)
            task.title = "Untitled Task"
            task.dateCreated = Date()
            task.dateDue = Date()
            task.completed = false
            task.archived = false
        }
        
        return handler
    }()
    
    //MARK: - Initializer to Load Core Data
    //Create an initializer to load Core Data, optionally able to use in-memory store
    //TODO: PRE-LAUNCH - Change inMemory back to false
    init(inMemory: Bool = false) {
        //Point the NSPersistentContainer to the Data Model
        container = NSPersistentContainer(name: "TestCore")
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("????ERROR: \(error.localizedDescription)!")
            } else {
                print("????SUCCESS: Successfully loaded Core Data")
            }
        }
    }
    
    //MARK: - Core Data Interactions
    func save() {
        let context = container.viewContext
        
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                print("????ERROR: Unable to save to Core Data!")
            }
        }
    }
    
}

TaskListView.swift - 负责显示任务

import SwiftUI
import CoreData

struct TaskListView: View {
    
    @Environment(\.managedObjectContext) var coreDataHandler
    @StateObject var viewModel = TaskListViewModel()
    
    @State var showAddTaskView = false
    @State var dismissView = false
    
    
    var body: some View {
        NavigationView {
            ZStack {
                List {
                    ForEach(viewModel.taskList) { task in
                        //TODO: Load in Custom Task View
                        TaskCardView(taskTitle: task.title ?? "No Title")
                            .toolbar {
                                EditButton()
                            }
                    }
                    .onDelete(perform: viewModel.deleteTask)
                    .background(Color.blue)
                }
                .listStyle(PlainListStyle())
            
                VStack {
                    Spacer()
                    
                    Button(action: {
                        showAddTaskView = true
                    }, label: {
                        Text("Add Task")
                            .frame(width: 150, height: 60)
                            .font(.headline)
                            .foregroundColor(.black)
                            .background(Color.yellow)
                            .cornerRadius(30)
                    })
                    .sheet(isPresented: $showAddTaskView, content: {
                        AddTaskView()
                    })
                }
            }
            
        }
        .navigationTitle("Tasks")
        .onAppear(perform: viewModel.fetchTasks)
        
    }
}

TaskListViewModel

import Foundation
import CoreData

final class TaskListViewModel : ObservableObject {
    
    var coreData = CoreDataHandler.shared
    @Published var taskList : [Task] = []
    
    init() {
        fetchTasks()
    }
    
    func fetchTasks() {
        let request = NSFetchRequest<Task>(entityName: "Task")
        do {
            taskList = try coreData.container.viewContext.fetch(request)
        } catch let error {
            print("????ERROR: Error fetching tasks for TaskListView - \(error)")
        }
    }
    
    func deleteTask(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let task = taskList[index]
        coreData.container.viewContext.delete(task)
        //Save change to Core Data
        coreData.save()
        //Fetch tasks to refresh taskList
        fetchTasks()
    }
}

AddTaskView.swift - 负责添加任务

import SwiftUI

struct AddTaskView: View {
    
    @Environment(\.managedObjectContext) var coreDataHandler
    @Environment(\.presentationMode) var presentationMode
    @StateObject var viewModel = AddTaskViewModel()
    @StateObject var taskListViewModel = TaskListViewModel()
    
    @State var titleTextFieldText: String = ""
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                TextField("Task Title", text: $titleTextFieldText)
                    .font(.headline)
                    .padding(.leading)
                    .frame(height: 55)
                    //TODO: Update to a specific color
                    .background(Color.gray)
                    .cornerRadius(10)
                    .padding(.horizontal)
                
                Button(action: {
                    viewModel.addTask(taskTitle: titleTextFieldText, taskDueDate: Date()) 
                    //Dismiss View if successful
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("Add Task")
                        .frame(width: 150, height: 60)
                        .font(.headline)
                        .foregroundColor(.black)
                        .background(Color.yellow)
                        .cornerRadius(30)
                })
                
                Spacer()
            }
        }
        .navigationTitle("Create New Task")
        .navigationBarTitleDisplayMode(.large)
    }
}

AddTaskViewModel.swift

import Foundation

final class AddTaskViewModel : ObservableObject {
    
    var coreDataHandler = CoreDataHandler.shared
    
    func addTask(taskTitle: String, taskDueDate: Date) {
        let newTask = Task(context: coreDataHandler.container.viewContext)
        
        newTask.title = taskTitle;
        newTask.dateCreated = Date();
        newTask.dateDue = taskDueDate;
        newTask.completed = false
        newTask.archived = false
        
        coreDataHandler.save()
    } 
}

【问题讨论】:

    标签: core-data mvvm swiftui


    【解决方案1】:

    据我所知,模型的 fetchTasks() 函数仅在模型首次加载时被调用一次。如果要获取最新数据,则每次数据更改时都需要调用它。您可以通过观察 NSManagedObjectContext.didSaveObjectsNotification 轻松做到这一点,它会在数据库更新时调用。

    import Foundation
    import CoreData
    import Combine
    
    final class TaskListViewModel : ObservableObject {
        
        var coreData = CoreDataHandler.shared
        @Published var taskList : [Task] = []
        private var cancellable: AnyCancellable?
        
        init() {
            fetchTasks()
    
            cancellable = NotificationCenter.default.publisher(for: NSManagedObjectContext.didSaveObjectsNotification, object: nil)
                .receive(on: DispatchQueue.main)
                .sink(receiveValue: { _ in
                self.fetchTasks()
            })
        }
    
        ...
    

    也就是说,有比一直调用 fetchTasks() 更好的选择。考虑在您的视图中使用@FetchRequest,或在您的模型中使用自动更新自身的 NSFetchedResultsController。

    这里有一个很好的选项:https://www.donnywals.com/responding-to-changes-in-a-managed-object-context/

    【讨论】:

    • 这就是我对如何从视图调用 fetchtasks() 感到有点困惑的地方,因为 taskList 在 TaskListViewModel 中?这篇文章理解起来有点混乱。不过我会看看使用@FetchRequest
    • 不要从视图中调用 fetchTasks。在模型中,您将创建一个 NSFetchedResultsController,或者您可以观察我在那里提到的通知。
    • fwiw 在 MVVM 路径下,不再使用它;非常同意@Adam - 对我们更好@FetchResults 将数据注入到哑/纯布局视图中。但是,如果您想在一个示例中达到顶峰,该示例几乎可以与 @FetchResults 替代方案一起完成您所追求的,请在此处查看我的演示项目 github.com/shufflingB/swiftui-core-data-with-model
    • @nix.codes 我为简单的 NSManagedObjectContext.didSaveObjectsNotification 解决方案添加了一个代码示例。但是,我仍然推荐 MVVM 的 NSFetchedResultsController 解决方案——你的 taskList 数组有点冒险,特别是如果你最终得到一个大型数据集或更复杂的模型。这是另一个 NSFetchedResultsController 示例,如果有帮助的话:cargath.github.io/blog/2019/06/25/…
    猜你喜欢
    • 2021-12-28
    • 1970-01-01
    • 2022-01-16
    • 1970-01-01
    • 1970-01-01
    • 2020-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多