#core-data #swiftui
#основные данные #swiftui
Вопрос:
На главном экране моего приложения отображается список объектов (в данном случае пациентов). Я хочу разрешить пользователю добавлять нового пациента, нажав кнопку на панели инструментов, которая позволяет им добавлять соответствующие сведения.
Внедрение представления редактирования было достаточно простым, и редактирование существующих пациентов работает без проблем. Также достаточно просто добавить нового пациента в представление списка без какого-либо автоматического модального отображения или перехода к представлению редактирования. Недостатком, конечно, является то, что пользователю потребуется вручную выполнить дополнительную последовательность шагов для редактирования вновь добавленного объекта, что не очень удобно для пользователя.
Это не проблема, если я использую MVVM, но я пытаюсь сделать это, используя собственный «рекомендуемый» подход Apple с оболочками свойств вокруг MOC и т. Д. Поскольку я не могу получить доступ к MOC до завершения инициализации представления, самый простой подход (создание нового пациента, если представление не предоставлено) невозможен. Текущая тактика (создание нового объекта в родительском представлении) приводит к несоответствию состояний и некоторому очень странному поведению, когда NavigationLink запускается всякий раз, когда вы пытаетесь отредактировать поля в дочернем представлении.
Я уверен, что я делаю это излишне сложным и все еще придерживаюсь императивного мышления, но некоторые рекомендации были бы полезны.
struct PatientsView: View {
@EnvironmentObject var dataController: DataController
@Environment(.managedObjectContext) var managedObjectContext
static let tag = "patients"
@State private var navLinkTag: Int? = 0
let patients: FetchRequest<Patient>
init() {
patients = FetchRequest(entity: Patient.entity(), sortDescriptors: [NSSortDescriptor(keyPath: Patient.creationDate, ascending: false)])
}
var body: some View {
NavigationView {
List {
ForEach(patients.wrappedValue) { patient in
PatientRow(patient: patient)
}
.onDelete { offsets in
for offset in offsets {
let item = patients.wrappedValue[offset]
dataController.delete(item)
}
}
}
.navigationTitle("Patients")
.toolbar {
ToolbarItem {
Button(action: {
navLinkTag = 1
}, label: {
Image(systemName: "plus")
})
.background(
NavigationLink(
destination: EditPatientView(patient: newPatient()),
tag: 1,
selection: $navLinkTag,
label: { EmptyView() })
)
}
}
}
}
private func newPatient() -> Patient {
let patient = Patient(context: managedObjectContext)
patient.creationDate = Date()
patient.name = "New patient"
dataController.save()
return patient
}
}
struct PatientRow: View {
@ObservedObject var patient: Patient
var body: some View {
HStack {
NavigationLink(destination: PatientActivityView(patient: patient)) {
VStack {
HStack {
Text(patient.patientName)
Text(patient.patientOwnerName)
.bold()
Spacer()
}
HStack {
Text(patient.patientSpecies)
Text(patient.patientSex)
Text(patient.age)
Spacer()
}
.font(.caption)
}
}
}
}
}
struct EditPatientView: View {
@EnvironmentObject var dataController: DataController
@Environment(.managedObjectContext) var managedObjectContext
@State var owner: String
@State var name: String
@State var species: String
@State var dob: Date
@State var sex: String
let patient: Patient
init(patient: Patient) {
self.patient = patient
_owner = State(wrappedValue: self.patient.patientOwnerName)
_name = State(wrappedValue: self.patient.patientName)
_species = State(wrappedValue: self.patient.patientSpecies)
_sex = State(wrappedValue: self.patient.patientSex)
_dob = State(wrappedValue: self.patient.patientDateOfBirth)
}
var body: some View {
Form {
Section(header: Text("Identity")) {
TextField("Name", text: $name.onChange(update))
TextField("Owner", text: $owner.onChange(update))
}
Section(header: Text("Signalment")) {
TextField("Species", text: $species.onChange(update))
TextField("Sex", text: $sex.onChange(update))
DatePicker("Date of birth",
selection: $dob.onChange(update),
in: ...Date(),
displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle()) }
}
.navigationTitle("Edit Patient")
}
private func update() {
patient.name = name
patient.owner = owner
patient.species = species
patient.sex = sex
patient.dateOfBirth = dob
}
}
Ответ №1:
Ваша проблема в том, что новые управляемые объекты и отредактированные обрабатываются по-разному. У меня были бы разные взгляды:
struct EditPatientView: View {
@EnvironmentObject var dataController: DataController
@Environment(.managedObjectContext) var managedObjectContext
@State private var owner: String
@State private var name: String
@State private var species: String
@State private var dob: Date
@State private var sex: String
let patient: Patient
init(patient: Patient) {
self.patient = patient
_owner = State(wrappedValue: self.patient.patientOwnerName)
_name = State(wrappedValue: self.patient.patientName)
_species = State(wrappedValue: self.patient.patientSpecies)
_sex = State(wrappedValue: self.patient.patientSex)
_dob = State(wrappedValue: self.patient.patientDateOfBirth)
}
var body: some View {
Form {
Section(header: Text("Identity")) {
TextField("Name", text: $name.onChange(update))
TextField("Owner", text: $owner.onChange(update))
}
Section(header: Text("Signalment")) {
TextField("Species", text: $species.onChange(update))
TextField("Sex", text: $sex.onChange(update))
DatePicker("Date of birth",
selection: $dob.onChange(update),
in: ...Date(),
displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle()) }
}
.navigationTitle("Edit Patient")
}
private func update() {
patient.setValue(self.name, forKey: "patientName")
patient.setValue(self.owner, forKey: "patientOwnerName")
patient.setValue(self.species, forKey: "patientSpecies")
patient.setValue(self.sex, forKey: "patientSex")
patient.setValue(self.dob, forKey: "patientDateOfBirth")
do {
try self.managedObjectContext.save()
} catch {
//Handle any error
}
}
}
и для новых пациентов:
struct NewPatientView: View {
@EnvironmentObject var dataController: DataController
@Environment(.managedObjectContext) var managedObjectContext
@State private var owner: String = ""
@State private var name: String = ""
@State private var species: String = ""
@State private var dob: Date = Date()
@State private var sex: String = ""
var body: some View {
Form {
Section(header: Text("Identity")) {
TextField("Name", text: $name)
TextField("Owner", text: $owner)
}
Section(header: Text("Signalment")) {
TextField("Species", text: $species)
TextField("Sex", text: $sex)
DatePicker("Date of birth",
selection: $dob,
in: ...Date(),
displayedComponents: .date)
.datePickerStyle(CompactDatePickerStyle())
}
Button(action: {
self.save()
}) {
Text("Save")
}
}
.navigationTitle("New Patient")
}
private func save() {
let patient = Patient(context: self.managedObjectContext)
patient.name = name
patient.owner = owner
patient.species = species
patient.sex = sex
patient.dateOfBirth = dob
do {
try self.managedObjectContext.save()
} catch {
//Handle any error
}
}
}
Я бы также подумал о том, чтобы сделать Владельца собственной Сущностью, которая связана с каждым животным через Отношения. В противном случае вы постоянно дублируете владельцев, если у них более одного животного. Я знаю, что знаю.
Кроме того, хотя я сохранил тот же шаблон обновления управляемого объекта для каждой записи, вы можете подумать о том, чтобы подождать обновления, пока пользователь не нажмет кнопку сохранения. Таким образом, у пользователя есть шанс очистить изменения и начать все сначала.
Комментарии:
1. Спасибо за предложение. Я должен был немного лучше сформулировать свой вопрос — я пытался избежать дублирования кода и хотел повторно использовать представление редактирования, если это вообще возможно, поскольку именно так я бы подошел к проблеме с помощью UIKit. Есть идеи? Что касается структуры данных, я полностью согласен — я нахожусь на первой итерации проекта («быстрый и грязный»!) И фокусируюсь на функциональности, связанной с пациентом. Я буду правильно сортировать отношения, как только ядро приложения заработает.
2. Единственная другая мысль, которая у меня была, — отправить управляемый объект в качестве необязательного. Вы можете проверить это и решить, хотите ли вы отредактировать или создать новый. Однако вы можете захотеть специализировать эти представления таким образом, чтобы вам было проще создавать отдельные представления, а не одно большое сложное представление. SwiftUI действительно поддерживает множество простых представлений, и вы можете сэкономить на кодировании, повторно используя общие подвиды. Для этого мало или нет затрат. Для регистрации нового пациента вам может потребоваться задать ряд вопросов, а не просто заполнить форму. Это действительно происходит с точки зрения пользователя.
Ответ №2:
Хорошо, думаю, я понял. Это кажется немного странным, но мне приходится танцевать вокруг жизненного цикла SwiftUI. Добавление нового экземпляра Patient в постоянное хранилище приведет к аннулированию PatientsView, что приведет к обновлению иерархии, и это было источником моих проблем раньше.
Я должен сказать, что время, которое я потратил на то, чтобы обдумать проблему, было интересным упражнением, но заставляет меня думать, что данные / бизнес-логика определенно должны храниться полностью отдельно от уровня представления при использовании SwiftUI. Архитектуры, подобные MVVM и / или Redux, полностью устраняют эту проблему.
Все изменения были внесены в PatientsView, поэтому я смог оставить PatientEditView нетронутым — в целом я доволен, но я очень открыт для предложений о том, как это можно улучшить.
import SwiftUI
struct PatientsView: View {
@EnvironmentObject var dataController: DataController
@Environment(.managedObjectContext) var managedObjectContext
@State private var showEditPatient = false
@State private var newPatient: Patient? = nil /// need to use @State as the PatientView FetchedResult will change when Patient is added or mutated causing the hierachy to be discarded and refreshed. A "normal" property will be re-created leading to multiple new Patient instances and warnings about state amendment during a view update
private let patients: FetchRequest<Patient>
init() {
patients = FetchRequest(entity: Patient.entity(), sortDescriptors: [NSSortDescriptor(keyPath: Patient.creationDate, ascending: false)])
}
var body: some View {
let createNewPatient = Binding<Bool>( /// A custom binding seems to be the best way of creating new Patient instances only when required
get: { return showEditPatient },
set: { newValue in
if !newValue {
newPatient = nil /// EditPatient view has been dismissed so discard the Patient
} else {
if newPatient == nil { newPatient = createPatient() } /// New Patient required
}
showEditPatient = newValue
}
)
return NavigationView {
List {
ForEach(patients.wrappedValue) { patient in
PatientRow(patient: patient)
}
.onDelete { offsets in
for offset in offsets {
let item = patients.wrappedValue[offset]
dataController.delete(item)
}
}
}
.navigationTitle("Patients")
.toolbar {
ToolbarItem {
Button(action: {
createNewPatient.wrappedValue = true
}, label: {
Image(systemName: "plus")
})
}
}
.sheet(isPresented: $showEditPatient,
onDismiss: { createNewPatient.wrappedValue = false }) {
EditPatientView(patient: newPatient!) /// Patient should never be nil - force unwrap will catch unexpected code path
}
}
}
private func createPatient() -> Patient {
let patient = Patient(context: managedObjectContext)
patient.creationDate = Date()
patient.name = "New patient"
dataController.save()
return patient
}
}
struct PatientsView_Previews: PreviewProvider {
static let dataController = DataController.preview
static var previews: some View {
PatientsView()
.environment(.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
}
}