UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 61 - CoreData Milestone Project - Unable to fetch Friend data

Forums > 100 Days of SwiftUI

Hello, everybody

I've been stuck at this project for a long time now and have already tried everything I can think of but I haven't found a solution. I am able to fetch the User data but the friends array for a user is either empty or it has an inaccurate count. I can't find the issue. Any help would be appreciated. Here's a link to my project on GitHub: https://github.com/CarmenRoxana/UsersAndFriendsCoreData/tree/master/UsersFriendsCoreData This is my function for fetching data:

`func fetchData() { guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else { print("Invalid URL") return }

    let request = URLRequest(url: url)

    URLSession.shared.dataTask(with: request) { data, response, error in
        // handle the result here.
        if let data = data {

            if let decodedResponse = try? JSONDecoder().decode([UserStruct].self, from: data), let users: [User] = try? self.moc.fetch(User.fetchRequest()) {
                // we have good data – go back to the main thread
                DispatchQueue.main.async {
                    for dUser in decodedResponse {
                        if users.first(where: { $0.id == dUser.id }) != nil {
                            continue
                        }

                        let user = User(context: self.moc)
                        user.id = dUser.id
                        user.isActive = dUser.isActive
                        user.name = dUser.name
                        user.age = Int16(dUser.age)
                        user.company = dUser.company
                        user.email = dUser.email
                        user.address = dUser.address
                        user.about = dUser.about
                        user.registered = dUser.registered
                        user.tags = dUser.tags

                        for dFriend in dUser.friends {
                            let friend = Friend(context: self.moc)
                            friend.id = dFriend.id
                            friend.name = dFriend.name
                            user.addToFriends(friend)
                        }
                    }

                    if self.moc.hasChanges {
                        try? self.moc.save()
                    }
                }

                // everything is good, so we can exit
                return
            }
        }

        // if we're still here it means there was a problem
        print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
    }.resume()
}`

3      

Hi Carmen,

I'm about to set out on this challenge today and I'll return here to let you know how it goes.

Perhaps my experience will help me to help you, once I've had a few hours to wrap my head around the problem.

Until then,

Russ

3      

Hi Carmen,

I'm almost finished Day 61 and I definitely ran into the same thing you have.

I'm planning to post a Twitter thread documenting some turning points and key discoveries, but let me post here what I hope will be helpful to your specific question.

The code you have works well enough to write to Core Data when only one entity is involved, Users, but as you've found, it gets sticky when trying to populate the Friends entity (and Tags entity if you choose to set that up later).

What I learned:

  1. Just like you can decode JSON directly into basic Swift structures you define yourself, you can decode JSON directly into Core Data objects. I found this to be an elegant solution. Here is what my NSManagedObject subclass looks like for the CDUser entity (where I'm keeping my user data):
//
//  CDUser+CoreDataClass.swift
//  Friends
//
//  Created by Russell Gordon on 2020-12-27.
//
//

import Foundation
import CoreData

@objc(CDUser)
public class CDUser: NSManagedObject, Decodable {

    // What properties to decode
    enum CodingKeys: CodingKey {
        case id
        case isActive
        case name
        case age
        case company
        case email
        case address
        case about
        case registered
        case tags
        case friends
    }

    required convenience public init(from decoder: Decoder) throws {

        // Attempt to extract the object
        guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else {
            fatalError("NSManagedObjectContext is missing")
        }

        // Actually create an instance of CDUser and tie it to the current managed object context
        self.init(context: context)

        // Decode JSON...
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        isActive = try container.decode(Bool.self, forKey: .isActive)
        name = try container.decode(String.self, forKey: .name)
        age = try container.decode(Int16.self, forKey: .age)
        company = try container.decode(String.self, forKey: .company)
        email = try container.decode(String.self, forKey: .email)
        address = try container.decode(String.self, forKey: .address)
        let untrimmedAbout = try container.decode(String.self, forKey: .about)
        about = untrimmedAbout.trimmingCharacters(in: CharacterSet(charactersIn: "\r\n"))

        // Decode the date
        // SEE: https://developer.apple.com/documentation/foundation/dateformatter#overview
        // SEE: https://developer.apple.com/documentation/foundation/iso8601dateformatter
        // SEE: https://makclass.com/posts/24-a-quick-example-on-iso8601dateformatter-and-sorted-by-function
        let dateAsString = try container.decode(String.self, forKey: .registered)
        let dateFormatter = ISO8601DateFormatter()
        registered = dateFormatter.date(from: dateAsString) ?? Date()

        // Decode all the tags and tie to this entity
        let tagsAsStrings = try container.decode([String].self, forKey: .tags)
        var tags: Set<CDTag> = Set()
        for tag in tagsAsStrings {
            let newTag = CDTag(context: context)
            newTag.name = tag
            // Associate each tag in the set with this user
            newTag.tagOrigin = self
            tags.insert(newTag)
        }
        tag = tags as NSSet
//        print("Count of tags for \(name!) is \(tag!.count)")

        // Decode all the friends and tie to this entity
        let friends = try container.decode(Set<CDFriend>.self, forKey: .friends)
        for friend in friends {
            // Associate each friend in the set with this user
            friend.friendOrigin = self
        }
        friend = friends as NSSet
//        print("Count of friends for \(name!) is \(friend!.count)")

    }

}

Here's what my extension to JSONDecoder looks like:

//
//  JSONDecoder.swift
//  Friends
//
//  Created by Russell Gordon on 2020-12-27.
//

import CoreData
import Foundation

// See: https://stackoverflow.com/a/52698618
// See: https://www.donnywals.com/using-codable-with-core-data-and-nsmanagedobject/
// Required to decode straight from JSON into Core Data managed objects
extension CodingUserInfoKey {
    static let context = CodingUserInfoKey(rawValue: "context")!
}

extension JSONDecoder {
    convenience init(context: NSManagedObjectContext) {
        self.init()
        self.userInfo[.context] = context!
    }
}

Here is how my entities are set up: User, Friend, Tag.

Now... on a related note...

  1. You might have noticed some print statements that I'd commented out in my CDUser subclass. Decoding JSON happens asynchronously. Until I realized what was happening, I was quite perplexed. Sometimes I would get 100 user objects in Core Data... sometimes 99! Sometimes a user would have 3 friends, sometimes that same user would have 5 friends! And... about 2/3 of the time, my app would crash with (just for fun) different error messages. After a lot of debugging and reading online, I now gather that updating a Core Data store with a lot of records should happen on a background thread. Apple has notes about concurrency here.

Here is what that looked like in practice for me:

// 1. Prepare a URLRequest to send our encoded data as JSON
let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")
var request = URLRequest(url: url!)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"

// 2. Run the request and process the response
URLSession.shared.dataTask(with: request) { data, response, error in

    // handle the result here – attempt to unwrap optional data provided by task
    guard let unwrappedData = data else {

        // Show the error message
        print("No data in response: \(error?.localizedDescription ?? "Unknown error")")

        return
    }

    // Now decode from JSON directly into Core Data managed objects
    // Do this on a background thread to avoid concurrency issues
    // SEE: https://stackoverflow.com/questions/49454965/how-to-save-to-managed-object-context-in-a-background-thread-in-core-data
    // SEE: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
    let parent = PersistenceController.shared.container.viewContext
    let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    childContext.parent = parent

    // Set merge policy for this context (so friends that exist on multiple users don't cause conflicts, for example)
    // Set the merge policy for duplicate entries – ensure that duplicate entries get merged into a single unique record
    // "To help resolve this, Core Data gives us constraints: we can make one attribute constrained so that it must always be unique. We can then go ahead and make as many objects as we want, unique or otherwise, but as soon as we ask Core Data to save those objects it will resolve duplicates so that only one piece of data gets written."
    childContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

    childContext.performAndWait {

        // ⭐️Decode directly into Core Data objects⭐️
        let decoder = JSONDecoder(context: childContext)
        if let decodedData = try? decoder.decode([CDUser].self, from: unwrappedData) {

            print("There were \(decodedData.count) users placed into Core Data")

        } else {
            print("Could not decode from JSON into Core Data")
        }

        // Required to actually write the changes to Core Data
        do {

            // Save Core Data objects
            try childContext.save()

        } catch {
            fatalError("Failure to save context: \(error)")
        }

    }

}.resume()

Finally, I should note that I'm working with the iOS 14 "full" SwiftUI app lifecycle.

Largely taken directly from the sample code in the iOS App Core Data template from Xcode 12, here are the contents of my CDPersistence.swift file:

//
//  Persistence.swift
//  CoreDataTest
//
//  Created by Russell Gordon on 2020-12-27.
//

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newUser = User.example
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Friends")

        // Set merge policy
        // Set the merge policy for duplicate entries – ensure that duplicate entries get merged into a single unique record
        // "To help resolve this, Core Data gives us constraints: we can make one attribute constrained so that it must always be unique. We can then go ahead and make as many objects as we want, unique or otherwise, but as soon as we ask Core Data to save those objects it will resolve duplicates so that only one piece of data gets written."
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                Typical reasons for an error here include:
                * The parent directory does not exist, cannot be created, or disallows writing.
                * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                * The device is out of space.
                * The store could not be migrated to the current model version.
                Check the error message to determine what the actual problem was.
                */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

That is what allows for the following key line just before decoding directly into Core Data objects – where we get a reference to the managed object context:

let parent = PersistenceController.shared.container.viewContext

Carmen, I hope I'm not oversharing and that this helps.

Good luck!

5      

Hi, Russell

Thank you very much for the detailed explanations, the links and for sharing your elegant solution. There's quite a bit I have to read and understand, my skills are quite primitive at this point it seems :) I don't know how to debug in XCode yet. Is there any resource you would recommend for me to learn from?

Thank you again,

Carmen

3      

Hi Carmen,

I am certain you know a lot! You've got this. You are more than halfway through this course, after all! 🚀 ☺️

Full disclosure, total honesty – a lot of the time, my debugging approach is just a few strategically placed print statements.

It seems some people frown on that, but I say – use what works for the situation at hand.

For example, this series of output:

successfully retrieved and saved avatar image to core data
successfully retrieved and saved avatar image to core data
successfully retrieved and saved avatar image to core data
retrieved avatar data
successfully retrieved and saved avatar image to core data
retrieved avatar data
retrieved avatar data
successfully retrieved and saved avatar image to core data
retrieved avatar data
retrieved avatar data

...helped me to understand that my code was working asynchronously (while some threads were still actively downloading placeholder user images from a website and saving those to Core Data, another thread in my program was already pulling those images from Core Data to show in a view).

I do make use of breakpoints and the Xcode debugger, but I have a lot to learn about what Xcode offers there myself. Here is a 3-minute video I made for my own students to introduce the debugger. (I teach high school computer science classes in Ontario, Canada).

Finally, Paul Hudson has a really nice overview of three approaches to debugging. I haven't read all of this, but it looks great!

Take care and good luck,

Russ

4      

I have been working on this for some hours now. I think the solution Russel posted is nice and clean but at the same time requires a lot more skills than I have at this point of the course. (I tried to follow it but now have more questions than before:D)

I'm wondering what Pauls intended approach was on this. I think there must be a way to do this with the skillset on has acquired at this point of the course.

Maybe with an intermediary struct where the json gets parsed to and that then is then written to Core Data?

5      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.