Создайте объект NSManagedObject и внедрите его в представление SwiftUI, которое автоматически добавляется в стек навигации

#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)
    }
}