我如何(或应该?)用嵌套的 managedObjectContext 替换这个 CoreData SwiftUI 应用程序中的 MVVM?

Posted

技术标签:

【中文标题】我如何(或应该?)用嵌套的 managedObjectContext 替换这个 CoreData SwiftUI 应用程序中的 MVVM?【英文标题】:How (or should?) I replace MVVM in this CoreData SwiftUI app with a Nested managedObjectContext? 【发布时间】:2020-12-27 12:30:55 【问题描述】:

我已经使用 CoreData 在 SwiftUI 应用程序中编写了这个 MVVM 的小示例,但我想知道是否有更好的方法来做到这一点,例如使用嵌套的视图上下文?

代码的目的是在用户更新所有需要的字段并点击“保存”之前不要触摸 CoreData 实体。换句话说,如果用户输入了很多属性然后“取消”,则不必撤消任何字段。但是如何在 SwiftUI 中解决这个问题?

目前,viewModel 有 @Published 变量,它们从实体中获取线索,但不受其属性的约束。

代码如下:

内容视图

这个视图很标准,但这里是 List 中的 NavigationLink 和 Fetch:

struct ContentView: View 
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Contact.lastName, ascending: true)],
        animation: .default)

    private var contacts: FetchedResults<Contact>

    var body: some View     List 
            ForEach(contacts)  contact in
                NavigationLink (
                    destination: ContactProfile(contact: contact)) 
                    Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
                
                
            
            .onDelete(perform: deleteItems)
         ///Etc...the rest of the code is standard

ContactProfile.swift 全文:

import SwiftUI

struct ContactProfile: View 
    
    @ObservedObject var contact: Contact
    @ObservedObject var viewModel: ContactProfileViewModel
    
    init(contact: Contact) 
        self.contact = contact
        self._viewModel = ObservedObject(initialValue: ContactProfileViewModel(contact: contact))
    
    
    @State private var isEditing = false
    
    @State private var errorAlertIsPresented = false
    @State private var errorAlertTitle = ""
    
    var body: some View 
        
        VStack 
            if !isEditing 
                Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
                    .font(.largeTitle)
                    .padding(.top)
                Spacer()
             else 
                Form
                    TextField("First Name", text: $viewModel.firstName)
                    TextField("First Name", text: $viewModel.lastName)
                
            
        
        .navigationBarTitle("", displayMode: .inline)
        .navigationBarBackButtonHidden(isEditing ? true : false)
        .navigationBarItems(leading:
                                Button (action: 
                                    withAnimation 
                                        self.isEditing = false
                                        viewModel.reset()  /// <- Is this necessary? I'm not sure it is, the code works
                                                                    /// with or without it. I don't see a 
                                                                    /// difference in calling viewModel.reset()
                                    
                                , label: 
                                    Text(isEditing ? "Cancel" : "")
                                ),
                            trailing:
                                Button (action: 
                                    if isEditing  saveContact() 
                                    withAnimation 
                                        if !errorAlertIsPresented 
                                            self.isEditing.toggle()
                                        
                                    
                                , label: 
                                    Text(!isEditing ? "Edit" : "Done")
                                )
        )
        .alert(
            isPresented: $errorAlertIsPresented,
            content:  Alert(title: Text(errorAlertTitle)) ) 
    
    private func saveContact() 
        do 
            try viewModel.saveContact()
         catch 
            errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
            errorAlertIsPresented = true
        
    

以及它使用的 ContactProfileViewModel

import UIKit
import Combine
import CoreData
/// The view model that validates and saves an edited contact into the database.
///

final class ContactProfileViewModel: ObservableObject 
    /// A validation error that prevents the contact from being 8saved into
    /// the database.
    
    enum ValidationError: LocalizedError 
        case missingFirstName
        case missingLastName
        var errorDescription: String? 
            switch self 
                case .missingFirstName:
                    return "Please enter a first name for this contact."
                case .missingLastName:
                    return "Please enter a last name for this contact."
            
        
    
    
    @Published var firstName: String = ""
    @Published var lastName: String = ""


    /// WHAT ABOUT THIS NEXT LINE?  Should I be making a ref here
    /// or getting it from somewhere else?

    private let moc = PersistenceController.shared.container.viewContext

    var contact: Contact
    
        init(contact: Contact) 
        self.contact = contact
        updateViewFromContact()
    
    
    // MARK: - Manage the Contact Form
    
    /// Validates and saves the contact into the database.
    func saveContact() throws 
        if firstName.isEmpty 
            throw ValidationError.missingFirstName
        
        if lastName.isEmpty 
            throw ValidationError.missingLastName
        
        contact.firstName = firstName
        contact.lastName = lastName
        try moc.save()
    
    
    /// Resets form values to the original contact values.
    func reset() 
        updateViewFromContact()
    
            
    // MARK: - Private
    
    private func updateViewFromContact() 
        self.firstName = contact.firstName ?? ""
        self.lastName = contact.lastName ?? ""
    

大部分视图模型代码改编自 GRDB 组合示例。所以,我并不总是确定要排除什么。包括什么。

【问题讨论】:

NSManagedObject 是 ObservableObject 并且 NSManaged 属性已发布,因此您可以在需要时直接在视图中观察 CoreData 对象。 是的,但是如果我尝试更改它们,似乎它们会立即保存到实体中,完全通过 managedObjectContext 传递。 【参考方案1】:

在发现后,我选择在这种情况下避免使用 viewModel:

moc.refresh(contact, mergeChanges: false)

Apple 文档:https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506224-refresh

所以你可以抛开ContactViewModel保留ContentView,并使用以下内容:

联系方式

enum 已扩展为 ContactProfile 视图。

import SwiftUI
import CoreData

struct ContactProfile: View 
    
    @Environment(\.managedObjectContext) private var moc
    
    @ObservedObject var contact: Contact
    
    @State private var isEditing = false
    
    @State private var errorAlertIsPresented = false
    @State private var errorAlertTitle = ""
    
    var body: some View 
        VStack 
            if !isEditing 
                Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
                    .font(.largeTitle)
                    .padding(.top)
                Spacer()
             else 
                Form
                    TextField("First Name", text: $contact.firstName ?? "")
                    TextField("First Name", text: $contact.lastName ?? "")
                
            
        
        .navigationBarTitle("", displayMode: .inline)
        .navigationBarBackButtonHidden(isEditing ? true : false)
        .navigationBarItems(leading:
                                Button (action: 

                                   /// This is the key change:

                                    moc.refresh(contact, mergeChanges: false)
                                    withAnimation 
                                        self.isEditing = false
                                    
                                , label: 
                                    Text(isEditing ? "Cancel" : "")
                                ),
                            trailing:
                                Button (action: 
                                    if isEditing  saveContact() 
                                    withAnimation 
                                        if !errorAlertIsPresented 
                                            self.isEditing.toggle()
                                        
                                    
                                , label: 
                                    Text(!isEditing ? "Edit" : "Done")
                                )
        )
        .alert(
            isPresented: $errorAlertIsPresented,
            content:  Alert(title: Text(errorAlertTitle)) ) 
    
    private func saveContact() 
        do 
            if contact.firstName!.isEmpty 
                throw ValidationError.missingFirstName
            
            if contact.lastName!.isEmpty 
                throw ValidationError.missingLastName
            
            try moc.save()
         catch 
            errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
            errorAlertIsPresented = true
        
    


extension ContactProfile 
    enum ValidationError: LocalizedError 
        case missingFirstName
        case missingLastName
        var errorDescription: String? 
            switch self 
                case .missingFirstName:
                    return "Please enter a first name for this contact."
                case .missingLastName:
                    return "Please enter a last name for this contact."
            
        
    

这还需要以下代码,可在此链接中找到:

SwiftUI Optional TextField

import SwiftUI

func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> 
    Binding(
        get:  lhs.wrappedValue ?? rhs ,
        set:  lhs.wrappedValue = $0 
    )

【讨论】:

在我看来,这是正确的选择,因为 viewModel 是 Contact 类(Core Data Managed Object)。很多时候,人们认为 Contact 类是模型,但模型是数据库。这就是托管对象类是可观察对象的原因。 Contact 类是一个 viewModel,因为它解释视图的模型(数据库),并响应视图的意图(将联系人保存并更新到数据库中)。

以上是关于我如何(或应该?)用嵌套的 managedObjectContext 替换这个 CoreData SwiftUI 应用程序中的 MVVM?的主要内容,如果未能解决你的问题,请参考以下文章

KTOR DSL - 如何嵌套模板?

mongodb:使用嵌套文档或带有引用的单独集合

如何在 Python 中创建一个 trie

带有或不带有 Twitter Bootstrap 的嵌套侧边栏

如何发送嵌套对象作为参数?

如何显示嵌套的复选框?