TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: How can I edit something that was already saved in CoreData?

Forums > Swift

@Koti  

Hi, so I have a recipe app that works perfectly. Below you have the code of the AddRecipeView.

import CoreData
import SwiftUI

struct AddRecipeView: View {

    @Environment(\.managedObjectContext) var moc //environment property to store our managed object context:
    @Environment(\.dismiss) var dismiss

    @State private var title = ""
    @State private var type = ""
    @State private var ingredients = ""
    @State private var text = ""
    @State private var author = ""
    @State private var hardness = 3
    @State private var diet = ""
    @State private var occasion = ""

    let diets = ["Vegetarian", "Vegan", "Gluten free",  "Dairy free", "Pescatarian", "Omnivore"]
    let types = ["Soup", "Salad", "Main", "Dessert", "Side", "Breakfast", "Lunch", "Dinner"]
    let occasions = ["Christmas", "New Years", "Birthday", "Easter", "Everyday"]

    var hasValidName: Bool {
        if title.isEmpty || author.isEmpty {
            return false
        }

        return true
    }

    var body: some View {
        NavigationView {
                Form {  // this is where I was getting the error for the form
                    Section {
                        TextField("Recipe name", text: $title)
                        TextField("Author's name", text: $author)

                        Picker("Diet", selection: $diet) {
                            Text("").tag("")
                            ForEach(diets, id: \.self) {
                                Text($0)
                            }
                        }
                        Picker("Type", selection: $type) {
                            Text("").tag("")
                            ForEach(types, id: \.self) {
                                Text($0)
                            }
                        }
                        Picker("Occasion", selection: $occasion) {
                            Text("").tag("")
                            ForEach(occasions, id: \.self) {
                                Text($0)
                            }
                        }
                    }

                    Section {
                        HardnessView(hardness: $hardness)
                    } header: {
                        Text("How hard is this?")
                    }

                    Section {
                        TextEditor(text: $ingredients)
                    } header: {
                        Text("Write the ingredients")
                    }
                    Section {
                        TextEditor(text: $text)
                    } header: {
                        Text("recipe")
                    }
                    Section {
                        Button("Save") {
                            let newRecipe = Recipe(context: moc)
                            newRecipe.id = UUID()
                            newRecipe.title = title
                            newRecipe.author = author
                            newRecipe.hardness = Int16(hardness)
                            newRecipe.diet = diet
                            newRecipe.occasion = occasion
                            newRecipe.ingredients = ingredients
                            newRecipe.type = type
                            newRecipe.text = text

                            try? moc.save()
                            dismiss()
                        }

                    }
                    .disabled(hasValidName == false)
                }
                .toolbar {
                    ToolbarItemGroup(placement: .navigationBarLeading) {
                        Button {
                            dismiss()
                        } label: {
                            Label("Back", systemImage: "xmark")
                        }
                        Text("Add recipe".uppercased())
                            .padding(95)

                    }
                }
            }
        }

}

Next I have the DetailView which populates everything that was saved in the add view.

import CoreData
import SwiftUI

struct DetailView: View {
    @State var recipe: Recipe

    @Environment(\.managedObjectContext) var moc
    @Environment(\.dismiss) var dismiss
    @State private var showingDeleteAlert = false

    var body: some View {
        ScrollView {
            Text(recipe.author ?? "Unknown author")
                .font(.headline)
                .foregroundColor(.secondary)
            HStack(alignment: .center) {
                Text(recipe.type?.uppercased() ?? "SOUP")
                    .font(.caption)
                    .fontWeight(.black)
                    .padding(8)
                    .foregroundColor(.white)
                    .background(.black.opacity(0.75))
                    .clipShape(Capsule())
                    .offset(x: -5, y: -5)
                Text(recipe.occasion?.uppercased() ?? "Unknown occasion")
                    .font(.caption)
                    .fontWeight(.black)
                    .padding(8)
                    .foregroundColor(.white)
                    .background(.black.opacity(0.75))
                    .clipShape(Capsule())
                    .offset(x: -5, y: -5)
                Text(recipe.diet?.uppercased() ?? "Unknown diet")
                    .font(.caption)
                    .fontWeight(.black)
                    .padding(8)
                    .foregroundColor(.white)
                    .background(.black.opacity(0.75))
                    .clipShape(Capsule())
                    .offset(x: -5, y: -5)
            }
                Text(recipe.ingredients ?? "Unknown ingredients")
                    .foregroundColor(.secondary)
                    .padding()

                Text(recipe.text ?? "No recipe text")
                    .foregroundColor(.secondary)
                    .padding()

            HardnessView(hardness: .constant(Int(recipe.hardness)))
                .font(.caption)

                .navigationTitle(recipe.title?.uppercased() ?? "Unknown Recipe")
                .navigationBarTitleDisplayMode(.inline)
                .alert("Delete recipe", isPresented: $showingDeleteAlert) {
                    Button("Delete", role: .destructive, action: deleteRecipe)
                    Button("Cancel", role: .cancel) { }
                } message: {
                    Text("Are you sure?")
                }
                .toolbar {
                    Button {
                        showingDeleteAlert = true
                    } label: {
                        Label("Delete this recipe", systemImage: "trash")
                    }
                }
        }
    }
    func deleteRecipe() {
        moc.delete(recipe)

        //comment this line if you want to make the deletion permanent
         try? moc.save()
        dismiss()
    }
}

What I would like to do is to add an Edit button to the DetailView that takes me back to the AddRecipeView, or a view that looks identical and everything is populated as previously saved for that recipe, so it can be edited then saved again. I don't know how to do that. I was told to just reference the previously saved entry, change whatever values I want and then resave the core data persistent container, but I have no clue how to do that either. I have been researching all day long and I just can't wrap my head around it.

I did try to make an EditRecipeView that starts off looking identical to the AddRecipeView, but I added :

struct EditRecipeView: View {
    @State var recipe: Recipe // added this

    @Environment(\.managedObjectContext) var moc //environment property to store our managed object context:
    @Environment(\.dismiss) var dismiss

and instead of all the binding I added things like this( so i can populate with the already saved data):

  TextField("Recipe name", text: recipe.title)b // error: Cannot convert value of type 'String?' to expected argument type 'Binding<String>'
  TextField("Author's name", text: recipe.author)
  Picker("Diet", selection: recipe.diet) {
      Text("").tag("")
       ForEach(diets, id: \.self) {
       Text($0)
       }
   }

this didnt work as I was getting errors like: Cannot convert value of type 'String?' to expected argument type 'Binding<String>' , and when I was trying to fix that this error keeps persisting on my Form: Trailing closure passed to parameter of type 'FormStyleConfiguration' that does not accept a closure.

3      

Hi! As an option you might reuse AddRecipeView for editing as well. Without full picture of passing the data around your views, I will use some assumptions.

First of all, why do you need to use @State var recipe: Recipe? To my understanding you can use let recipe: Recipe as your have class object from core data which is of reference type. Secondly, Core Data objects have their own ids by default and are Identifiable by default, you'd better call attribute not id but itemId if you really need to use them, otherwise just delete as it might lead to strange behavior of those items later.

To the main point. You can call your view AddEditRecipeView and make the following changes, I will skip most part just to show how you can pass data into view depending on what action you are going to do Add or Edit.

struct AddEditRecipeView {
  ...
  let recipe: Recipe? // If you are adding new item you will pass nil, else you will pass the item for editing
  @State private var newRecipe = false

  var body: some View {
    NavigationView {
      ...

      Button(newRecipe ? "Add" : "Update") {
        if newRecipe {
           // here you create newRecipe
           let newRecipe = Recipe(context: moc)
           newRecipe.title = title
           newRecipe.author = author
           ...
        } else {
          // here you update existing recipe
          recipe?.title = title
          recipe?.author = author
          ...
        }

          try? moc.save()
          dismiss()
      }
    }
    .onAppear {
      newRecipe = (recipe == nil) // here we check if we passed recipe to this view
      // and if there is recipe that you are passing you assign those properties of recipe to your view fields
      if newRecipe == false {
        title = recipe?.title ?? ""
        author = recipe?.author ?? ""
      }
    }
  }
}

and then you can pass item accordingly. If you create new item:

AddEditRecipeView (recipe: nil)

if you edit an item

AddEditRecipeView (recipe: recipe)

Hope this will help.

3      

@Koti  

HELLO @ygeras , Your awesome support and expertise seriously saved the day and helped me nail that problem. Thank you!

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.