使用 CoreDataViewModel 时如何在 Canvas 中预览 Core Data 对象

Posted

技术标签:

【中文标题】使用 CoreDataViewModel 时如何在 Canvas 中预览 Core Data 对象【英文标题】:How to preview Core Data objects in Canvas when using a CoreDataViewModel 【发布时间】:2021-09-17 11:13:09 【问题描述】:

以下代码有效地使用@StateObject@ObservedObject 包装器来捕获来自ObservableObject 视图模型的更改。一切都按预期工作,除了我希望能够在 Canvas 中显示虚拟数据以用于设计目的。正如您从下面的代码中看到的那样,我能够创建一个Car 对象inMemory 并在Preview 结构中有效地检索它,我的问题是在尝试显示多辆汽车时,如您所见,显示多辆汽车 我不得不在Preview 结构中复制List 的代码以用于CarsView。在下面的代码中,这似乎没什么大不了的,因为我删除了很多构成 List 的代码,但在我的生产代码中,我有很多代码来自定义行。

这真的是显示通常由 ViewModel 管理的数据的最佳方式吗?

有没有一种无需重复大量SwiftUI 代码即可显示数据的方法?

SwiftUI 视图

父视图/内容视图

    struct ContentView: View 
        @StateObject var coreDataViewModel = CoreDataViewModel()

        var body: some View 
            
            TabView(selection: 1)
                
                CarsView(coreDataViewModel: coreDataViewModel)
                
                .tabItem 
                    Text("Cars")
                .tag(1)

                // other tabs...
            
        
    

第二视图/汽车视图

我的问题是,在这里,我从 Preview 结构中的主视图复制代码。

    struct CarsView: View 
        @ObservedObject var coreDataViewModel:CoreDataViewModel
        var body: some View 
            List 
                ForEach(coreDataViewModel.cars)  car in
                    // custom row to display cars
                
            
        
    

    struct CarsView_Previews: PreviewProvider 
        @Environment(\.managedObjectContext) private var viewContext
        static var previews: some View 

            CarsView(coreDataViewModel: CoreDataViewModel())
            
            
            let context = CoreDataManager.preview.container.viewContext
            let requestCar: NSFetchRequest<Car> = Car.fetchRequest()
            let fetchedCar = (try! context.fetch(requestCar).first)!
            
            let cars = [fetchedCar]
            // repeated code 
            List
                ForEach(cars)  car in
                    Text(car.make ?? "")
                
            
        
    

核心数据

实体和属性

汽车

制作:字符串 型号:字符串

核心数据管理器

    class CoreDataManager

        static let instance = CoreDataManager()
        
        static var preview: CoreDataManager = 
            let result = CoreDataManager(inMemory: true)
            let viewContext = result.container.viewContext
            // new car
            let car = Car(context: viewContext)
            car.make = "Ford"
            car.model = "Mustang"
            
            do 
                try viewContext.save()
             catch 
            
            
            return result
        ()
        
        let container: NSPersistentContainer
        let context: NSManagedObjectContext
        
        init(inMemory: Bool = false)
            container = NSPersistentContainer(name: "CoreDataContainer")
            
            if inMemory 
                container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            
            
            container.loadPersistentStores  (description, error) in
                if let error = error
                    print("Error loading Core Data. \(error)")
                
            
            context = container.viewContext
        
        
        func save()
            do
                try context.save()
                print("Saved successfully!")
            catch let error
                print("Error saving Core Data. \(error.localizedDescription)")
            
        
    

核心数据视图模型

    class CoreDataViewModel: ObservableObject
        
        let manager = CoreDataManager.instance
        
        @Published var cars: [Car] = []
        
        init()
            getCars()
        
        
        // addCar, deleteCar, updateCar etc. methods...

        func getCars()
            let request = NSFetchRequest<Car>(entityName: "Car")

            let sort = NSSortDescriptor(keyPath: \Car.model, ascending: true)
            request.sortDescriptors = [sort]

            do
                cars =  try manager.context.fetch(request)
            catch let error
                print("Error fetching businesses. \(error.localizedDescription)")
            
        
        
        func save()
            self.manager.save()
        
    

【问题讨论】:

如果我理解正确,您担心代码重复。重复发生在哪里:在模型中,在视图中? 在 CarsView 的预览中,我有一条评论说 //repeated code。当然,这里我只展示了 List 的最少代码,但是代码太多了,当同时更新 UI 时,View 和 Preview 都感觉不对。 它就在那里,查看第二个视图CarsView 的预览。我留下 CoreData 代码仅供参考,以便更好地了解我是如何创建对象的。 可能是***有问题,我在CarsView的预览中看到重复的代码,是我帖子里的第二个sn-p代码。 问题的原因在于 View 和 CoreData 之间的紧密耦合。为了让您了解:不要将视图模型命名为“CoreDataViewModel”(how) - 而是将其命名为“CarViewModel” - (what!)和 完全隐藏您正在使用托管对象和 CoreData 的事实。然后对于预览(和测试),在 DI 的帮助下,您可以在创建时设置一个“模拟存储库”in CarViewModel。 【参考方案1】:

预览的目的是使用带有一些给定数据的普通视图,而不是从实际视图中复制代码。除此之外,您似乎还有很多东西。

我会先将 Core Data 管理器类注入到视图模型中:

class CoreDataViewModel: ObservableObject        
    let manager: CoreDataManager
    
    @Published var cars: [Car] = []
    
    init(coreDataManager: CoreDataManager = .instance)
        self.manager = coreDataManager
        getCars()
    

然后我会将预览简化为

struct CarsView_Previews: PreviewProvider 
    static var previews: some View 
        CarsView(coreDataViewModel: CoreDataViewModel(coreDataManager: .preview))
    

您已经在 preview 中创建了一个 Car 对象,为什么不在循环中执行此操作,这里我已将其移至一个单独的函数中,该函数可以从 preview 声明内部或单独调用

#if DEBUG
extension CoreDataManager 
    func createMockCars() 
        for i in 1...5 
             let car = Car(context: self.context)
             car.make = "Make \(i)"
             car.model = "Model \(i)"
        
        try! self.context.save()
    

#endif

正如其中一个 cmets 所述,您应该考虑将您的视图模型重命名为与 CarsViewModel 或 CarListViewModel 等汽车有关的名称。

【讨论】:

成功了,非常感谢您的帮助! @ Joakim Danielson - 我非常感谢你,这对我在 CoreData 中的所有数据对象都非常有效,太棒了!不幸的是,我对依赖注入并不是很熟悉。快速提问,除了帮助 Canvas 中的预览之外,将 Core Data 管理器类注入视图模型还有其他好处吗?在这里,我只是想更好地理解我们对视图模型所做的注入。更好的是,这真的被认为是依赖注入吗? 是的,这是依赖注入,正如您在此实现中看到的那样,您在视图模型和 CoreDataManager 类之间没有硬编码的依赖关系,因此您可以注入不同的类型。这不仅对预览有用,而且对测试有用。为了更进一步,可以使用协议来代替,这样您就可以注入任何符合该协议的类型。 感谢您的澄清和良好的建议。我将仔细研究DI。非常感谢您的所有帮助!

以上是关于使用 CoreDataViewModel 时如何在 Canvas 中预览 Core Data 对象的主要内容,如果未能解决你的问题,请参考以下文章

如何在使用 observable 更新数组时更新 *ngFor 以及如何在网站加载时初始化 observable?

在使用 jupyter notebook 时如何在 pandas 中使用 Dataframe 时查看完整数据? [复制]

按下标签栏项目时如何显示警报视图以及在其他目标中使用时如何忽略它?

在 Rails 6 中使用 activestorage 时,如何在重新显示表单时保留文件?

使用webdriver查找元素时如何在xpath中使用撇号(')?

使用 DialogBoxIndirect 时,如何在对话框关闭时获取用户输入的文本?