M6L3 : Preloading Data

As we begin Lesson 3 of Module 6 of the CWC+ iOS Databases course, Chris highlights a potential future error for us to address.

We copied our Persistence.swift file from another (new) project but, in order to ensure that the app runs correctly, we need to change a property in the init() method of the Persistence.swift file.

...
init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "Recipe Data Model")
    if inMemory {
        ...

The highlighted name value shown above needs to be the same as the .xcdatamodeId data file in our project. If it is, we’re good to go.

Parsing JSON into Core Data on first run

The main element of this lesson is to determine if this is the first time the app has been run and, if so, preload our default data into Core Data so that the app has something to work with. Future launches of the app will then just use that which is in Core Data.

The way we do this is as follows:

init() {
    checkLoadedData()
}
func checkLoadedData() {
    let status = UserDefaults.standard.bool(forKey: Constants.isDataPreloaded)
    if status == false {
        preloadLocalData()
    }
}
func preloadLocalData() {
    let localRecipes = DataService.getLocalData()
    for r in localRecipes {
        let recipe = Recipe(context: managedObjectContext)
        recipe.cookTime = r.cookTime
        recipe.directions = r.directions
        recipe.featured = r.featured
        recipe.highlights = r.highlights
        recipe.id = UUID()
        recipe.image = UIImage(named: r.image)?.jpegData(compressionQuality: 1.0)
        recipe.name = r.name
        recipe.prepTime = r.prepTime
        recipe.servings = r.servings
        recipe.summary = r.description
        recipe.totalTime = r.totalTime
        for i in r.ingredients {
            let ingredient = Ingredient(context: managedObjectContext)
            ingredient.id = UUID()
            ingredient.name = i.name
            ingredient.unit = i.unit
            ingredient.num = i.num ?? 1
            ingredient.denom = i.denom ?? 1
            recipe.addToIngredients(ingredient)
        }
    }
    do {
        try managedObjectContext.save()
        UserDefaults.standard.setValue(true, forKey: Constants.isDataPreloaded)
    }
    catch {
        // Couldn't save to core data
    }
}

The above presumes that getLocalData() is our JSON parser in the DataService class, and that Recipe & Ingredient are entities in our Core Data.

UserDefaults is a persistent SwiftUI interface that stores key-value pairs across launches of the app (re: Apple Developer Documentation). We’re using it to store our flag constant isDataPreloaded so that we can check if we’ve been here before. This will prove very useful in future apps.

addToIngredients() is an “accessor” that was generated by Xcode when we previously set up our Entity relationships, making it easy to, well, to add the ingredients.

A clever piece of code is where the image in the asset library is converted into core data, re:

recipe.image = UIImage(named: r.image)?.jpegData(compressionQuality: 1.0)

For this to work, we need to import SwiftUI at the top of the file (where we have import Foundation). Chris does this with import UIKit but mentions it should be available in SwiftUI, and it is. This is the Apple Developer Documentation page for more info.

Xcode throws a wobbly

On our test run, we get a bunch of Xcode warning errors in the console:

2022-04-29 16:49:21.936301+0100 EnhancedRecipeListApp[23921:392411] [error] fault: One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.
CoreData: fault: One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.
2022-04-29 16:49:21.937309+0100 EnhancedRecipeListApp[23921:392411] [error] CoreData: One or more models in this application are using transformable properties with transformer names that are either unset, or set to NSKeyedUnarchiveFromDataTransformerName. Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead. At some point, Core Data will default to using "NSSecureUnarchiveFromData" when nil is specified, and transformable properties containing classes that do not support NSSecureCoding will become unreadable.
CoreData: warning: Property 'directions' on Entity 'Recipe' is using nil or an insecure NSValueTransformer.  Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead.
CoreData: warning: Property 'highlights' on Entity 'Recipe' is using nil or an insecure NSValueTransformer.  Please switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead.

Chris gets the same messages in the tutorial, but remarks that he doesn’t know what it means. Given the time between his tutorial and today, it’s a surprise that this hasn’t become a bigger issue by now. I’m guessing it will at some point, and it’ll be a job to figure out what to do to fix it. Not a task for a newbie…