1

I'm working on an iOS app that track people's medication and I got an add view and an edit view, both look almost the same with the exception that on my edit view I use the .onAppear to load all the medication data into the fields with an existing medication using let medication: Medication

My Form looks something like this:

        Form {
            Group {
                TextField("Medication name", text: $name).disableAutocorrection(true)
                TextField("Remaining quantity", text: $remainingQuantity).keyboardType(.numberPad)
                TextField("Box quantity", text: $boxQuantity).keyboardType(.numberPad)
                DatePicker("Date", selection: $date, in: Date()...).datePickerStyle(GraphicalDatePickerStyle()) 
                Picker(selection: $repeatPeriod, label: Text("Repeating")) {
                        ForEach(RepeatPeriod.periods, id: \.self) { periods in
                            Text(periods).tag(periods)  
                }
            .onAppear {
                if pickerView {
                    self.name = self.medication.name != nil ? "\(self.medication.name!)" : ""
                    self.remainingQuantity = (self.medication.remainingQuantity != 0) ? "\(self.medication.remainingQuantity)" : ""
                    self.boxQuantity = (self.medication.boxQuantity != 0) ? "\(self.medication.boxQuantity)" : ""
                    self.date = self.medication.date ?? Date()
                    self.repeatPeriod = self.medication.repeatPeriod ?? "Nunca"
                    self.notes = self.medication.notes != nil ? "\(self.medication.notes!)" : ""
                }
            }
        }

I thought of using a binding variable like isEditMode and it works fine but I had some issue related to the moc object when calling the add view that doesn't provide an object.

Here's how my editView preview looks like

struct EditMedicationSwiftUIView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

static var previews: some View {
    let medication = Medication(context: moc)
    
    return NavigationView {
        EditMedicationSwiftUIView(medication: medication)
    }
    
    
}

}

Any suggestions?

  • 1
    Welcome to SO - Please take the [tour](https://stackoverflow.com/tour) and read [How to Ask](https://stackoverflow.com/help/how-to-ask) to improve, edit and format your questions. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. – lorem ipsum Sep 25 '21 at 22:51
  • Form experience, adding an editing feature is about 5x more complex than just rendering immutable data. Thus, I would suggest to utilise appropriate patterns which have the potential to solve this kind of problem cleanly: MVVM, MVI, etc. - which includes to move all logic to the "ViewModel" and all "services" will be accessed through an abstraction layer, that is, a "Model" or a "Store". So, there is no "FetchRequest" in your view - just plain simple immutable data that get rendered. – CouchDeveloper Sep 26 '21 at 09:06

1 Answers1

1

Here is a simplified version of what I think you are trying to do. It uses code from a SwiftUI sample project. Just create an Xcode SwiftUI project with CoreData.

import SwiftUI
import CoreData
//Standard List Screen where you can select an item to see/edit and you find a button to add
struct ReusableParentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    //Keeps work out of the Views so it can be reused
    @StateObject var vm: ReusableParentViewModel = ReusableParentViewModel()
    var body: some View {
        NavigationView{
            List{
                ForEach(items) { item in
                    NavigationLink {
                        //This is the same view as the sheet but witht he item passed fromt he list
                        ReusableItemView(item: item)
                        
                    } label: {
                        VStack{
                            Text(item.timestamp.bound, formatter: itemFormatter)
                            Text(item.hasChanges.description)
                        }
                        
                    }
                }.onDelete(perform: { indexSet in
                    for idx in indexSet{
                        vm.deleteItem(item: items[idx], moc: viewContext)
                    }
                })
            }
            //Show sheet to add new item
            .sheet(item: $vm.newItem, onDismiss: {
                
                vm.saveContext(moc: viewContext)
                //You can also cancel/get rid of the new item/changes if the user doesn't save
                //vm.cancelAddItem(moc: viewContext)
            }, content: { newItem in
                NavigationView{
                    ReusableItemView(item: newItem)
                    
                }
                //Inject the VM the children Views have access to the functions
                .environmentObject(vm)
            })
            
            .toolbar(content: {
                ToolbarItem(placement: .automatic, content: {
                    //Trigger new item sheet
                    Button(action: {
                        vm.addItem(moc: viewContext)
                    }, label: {
                        Image(systemName: "plus")
                    })
                })
            })
        }
        //Inject the VM the children Views have access to the functions
        .environmentObject(vm)
    }
    private let itemFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .medium
        return formatter
    }()
}
//The Item's View
struct ReusableItemView: View {
    //All CoreData objects are ObservableObjects to see changes you have to wrap them in this
    @ObservedObject var item: Item
    @Environment(\.editMode) var editMode
    var body: some View {
        VStack{
            if editMode?.wrappedValue == .active{
                EditItemView(item: item)
            }else{
                ShowItemView(item: item)
            }
        }
        .toolbar(content: {
            ToolbarItem(placement: .automatic, content: {
                //If you want to edit this info just press this button
                Button(editMode?.wrappedValue == .active ? "done": "edit"){
                    if editMode?.wrappedValue == .active{
                        editMode?.wrappedValue = .inactive
                    }else{
                        editMode?.wrappedValue = .active
                    }
                }
            })
        })
    }
}
//The View to just show the items info
struct ShowItemView: View {
    //All CoreData objects are ObservableObjects to see changes you have to wrap them in this
    @ObservedObject var item: Item
    var body: some View {
        if item.timestamp != nil{
            Text("Item at \(item.timestamp!)")
        }else{
            Text("nothing to show")
        }
    }
}
//The View to edit the item's info
struct EditItemView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @EnvironmentObject var vm: ReusableParentViewModel
    @Environment(\.editMode) var editMode
    //All CoreData objects are ObservableObjects to see changes you have to wrap them in this
    @ObservedObject var item: Item
    var body: some View {
        DatePicker("timestamp", selection: $item.timestamp.bound).datePickerStyle(GraphicalDatePickerStyle())
    }
}
struct ReusableParentView_Previews: PreviewProvider {
    static var previews: some View {
        ReusableParentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}
class ReusableParentViewModel: ObservableObject{
    //Can be used to show a sheet when a new item is created
    @Published var newItem: Item? = nil
    //If you dont want to create a CoreData item immediatly just present a sheet with the AddItemView in it
    @Published var presentAddSheet: Bool = false
    func addItem(moc: NSManagedObjectContext) -> Item{
        //You should never create an ObservableObject inside a SwiftUI View unless it is using @StateObject which doesn't apply to a CoreData object
        let temp = Item(context: moc)
        temp.timestamp = Date()
        //Sets the newItem variable
        newItem = temp
        //And returns the new item for other uses
        return temp
    }
    func cancelAddItem(moc: NSManagedObjectContext){
        rollbackChagnes(moc: moc)
        newItem = nil
    }
    func rollbackChagnes(moc: NSManagedObjectContext){
        moc.rollback()
    }
    func deleteItem(item: Item, moc: NSManagedObjectContext){
        moc.delete(item)
        saveContext(moc: moc)
    }
    func saveContext(moc: NSManagedObjectContext){
        do{
            try moc.save()
        }catch{
            print(error)
        }
    }
}

And if for some reason you don't want to create a CoreData object ahead of time which seems to be what you are doing you can always Create the temp variables and make a sharable editable view that takes in @Binding for each variable you want to edit.

//The View to Add the item's info, you can show this anywhere.
struct AddItemView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @EnvironmentObject var vm: ReusableParentViewModel
    
    //These can be temporary variables
    @State var tempTimestamp: Date = Date()
    var body: some View {
        EditableItemView(timestamp: $tempTimestamp)
            .toolbar(content: {
                ToolbarItem(placement: .navigationBarLeading, content: {
                    //Create and save the item
                    Button("save"){
                        let new = vm.addItem(moc: viewContext)
                        new.timestamp = tempTimestamp
                        vm.saveContext(moc: viewContext)
                    }
                })
            })
    }
}
//The View to edit the item's info
struct EditItemView: View {
    @EnvironmentObject var vm: ReusableParentViewModel
    @Environment(\.managedObjectContext) private var viewContext
    @ObservedObject var item: Item
    var body: some View {
        VStack{
        EditableItemView(timestamp: $item.timestamp.bound)
            .onDisappear(perform: {
                vm.rollbackChagnes(moc: viewContext)
            })
            //Just save the item
            Button("save"){
                vm.saveContext(moc: viewContext)
            }
        }
    }
}
//The View to edit the item's info
struct EditableItemView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @EnvironmentObject var vm: ReusableParentViewModel
    //All CoreData objects are ObservableObjects to see changes you have to wrap them in this
    @Binding var timestamp: Date
    var body: some View {
        DatePicker("timestamp", selection: $timestamp).datePickerStyle(GraphicalDatePickerStyle())
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48