Using Navigation View and CoreData

I’m writing an address book app for practice, kind of mimicking the Contacts app.

I got a Navigation view working to display existing contacts in the database with update and delete working fine.

Are there examples of a user clicking, say a plus sign (+) to add a new object? How can I pass a new empty object to my editing view?

This doesn’t work: AddressView(oldAddress: Address(context: addressContext)) trying to call:

struct AddressView: View {

var oldAddress: Address

Hey @JimOlivi love that you’re taking on this challenge! So cool, I often learn the best from challenges myself :slight_smile: let us know how it goes via a Journal!

In the iOS Databases module six, Chris teaches you how to accept user input via a Form, then create a new Core Data object, and save it as a Recipe. I just finished this module myself, so you can see my full repo here.

To do this, I use a Button to execute a method that will save the object to the Core Data store. I use NavigationViews, when I want the user to go to a new View after clicking something instead, and Buttons, when I want a piece of code executed, e.g. saving to the Core Data database.

// Add button
Button("Add") {
    // Calls the method to add the new recipe to core data
    addRecipe()
    
    // Calls the method that clears the State properties in the  form
    clear()
    
    // Switch the View to the list view
    tabSelection = Constants.listTab
}

To do all this, first, you define @State properties to hold the user input:

// MARK: - Define Recipe Properties
@State private var name = ""
@State private var summary = ""
@State private var prepTime = ""
@State private var cookTime = ""
@State private var totalTime = ""
// Even though this is an Integer in the Data Store, accept the input as a string
@State private var servings = "" 

Next, you need a method to accept this new data, create a new Core Data object, then save it:

/// Saves the Recipe to Core Data based on the filled out information, then transitions the View back to the
/// Recipe List View.
func addRecipe() {
    // Create a new Core Data Recipe object
    let newRecipe = Recipe(context: viewContext)
    
    // Add all the properties to this new Core Data object using the State properties
    newRecipe.name = name
    newRecipe.summary = summary
    newRecipe.prepTime = prepTime
    newRecipe.cookTime = cookTime
    newRecipe.totalTime = totalTime
    newRecipe.servings = Int(servings ) ?? 1
    newRecipe.highlights = highlights
    newRecipe.directions = directions
    // Convert the UIImage to a binary type of image
    newRecipe.image = recipeImage?.pngData()
    
    // For each of the directionsingredients add it to the recipe
    for ingredient in ingredients {
        // Create Core Data Ingredient
        let coreDataIngredient = Ingredient(context: viewContext)
        
        coreDataIngredient.id = UUID()
        coreDataIngredient.name = ingredient.name
        coreDataIngredient.num = ingredient.num ?? 1
        coreDataIngredient.denom = ingredient.denom ?? 1
        coreDataIngredient.unit = ingredient.unit
        // Reference back to the parent recipe
        //            coreDataIngredient.recipe = newRecipe
        
        // Add the ingredient to the recipe
        newRecipe.addToIngredients(coreDataIngredient)
        
    }
    
    // Save to Core Data
    do {
        // Attempts to save
        try viewContext.save()
        
    }
    catch {
        // Displays any errors
        print(error.localizedDescription)
    }
}

How the whole form looks (in Chris fashion, he shows you SO much other stuff too, like accepting a photo from the user, or allowing him or her to use a camera):

Another Issue…

I think I’m having problems with concurrency vis-a-vis CoreData.

Adding and changing objects work fine, but deletes are another matter. When I delete an object, it immediately disappears from the List View. But after “force quitting” the simulator and restarting it (iPhone 13) the deleted objects re-appear. It’s as if the buffer is not being posted back to the Persistent Store, or something like that.

Ah yes, a weird issue indeed! However, Chris addresses this as well. He doesn’t say this exactly, but for any database operation, you must commit it to the database. That’s a special action that executes the queries upon the database. Sometimes it’s faster to only commit after a certain point in time, because it can be slower. So for example, there’s a number of new objects added, deleted, changed, then you can commit all of this at once to the database, versus committing every time, and making all these transactions slower. Still, I don’t know how much of a performance cost it would be to commit after every change in Swift, I was speaking on experience with other databases, especially those over a network connection.

So to me, this answer will likely be fixed by you doing a save to the database after these delete operations. Fingers-crossed this does the trick.

Cheers,
Andrew

Your suggested solution sounds reasonable.

Would this also mean that we might want to perform a bunch of additions and edits, then hold back saves, or possibly do the saves in the background.

Just to remind me, whats the event that tells my code that a user is quitting an app, or at least if my app has lost focus? And, is there a way to ask CoreData if there are uncommitted transactions?

1 Like

Yep, exactly. That’s the thought at least, when I write apps that are not running on the user’s machine. Still, @Chris_Parker @mikaelacaron or @joash might know the best practices for SwiftUI

It’s a longggg piece of code. I just grabbed it from the iOS Databases Module 4 firebase learning app:

// Listens for Published events, like the user making the app go into the background, once this happens, we save to the database
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
    // Save progress to the database, when app moves to the background
    model.saveData(writeToDatabase: true)

That’s a good question… I tried searching for something, but couldn’t find it. This was the closest I could find Consuming relevant store changes | Apple Developer Documentation

I agree with this approach. Even in UIKit, the code for this behaviour is included by default if you choose to create a new project in UIKit (storyboards), and you can see this as being generated as part of AppDelegate (for older Xcodes), and SceneDelegate (for newer ones).

You can actually use the piece of code that @Redfox1 shared that was part of iOS Databases Module 4, but refactor it for Core Data.

Have you tried context.hasChanges ?

func saveContext() {
  let context = persistentContainer.viewContext
  if context.hasChanges {
    do {
      try context.save()
    } catch { ... }
  }
}

Here’s the link for the documentation of context.hasChanges: