我如何(或应该?)用嵌套的 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?的主要内容,如果未能解决你的问题,请参考以下文章