如何(或应该?)用嵌套的managedObjectContext替换此CoreData SwiftUI应用程序中的MVV



我已经在SwiftUI应用程序中使用CoreData编写了这个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 ?? ""
}
}

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

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

moc.refresh(contact, mergeChanges: false)

苹果文档: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可选文本字段

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

最新更新