Luis Ramirez
Luis Ramirez

Reputation: 1614

Core data how to use NSMangedObjectContext in multithreaded

Okay, I've been going at this for a day and can't seem to figure out what I am doing wrong. This is how my data model looks like for core data.

enter image description here

This is how my code looks like.

class Service {
static let shared = Service()
private let numberOfPokemons = 151

func downloadPokemonsFromServer(completion: @escaping ()->()) {
    let urlString = "https://pokeapi.co/api/v2/pokemon?limit=\(numberOfPokemons)"
    guard let url = URL(string: urlString) else { return }
    var id: Int16 = 0


    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let err = error {
            print("Unable to fetch pokemon", err)
        }

        guard let data = data else { return }
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase

        do {
            let pokemonJSON = try decoder.decode(PokemonsJSON.self, from: data)
            pokemonJSON.pokemons.forEach { (JSONPokemon) in
                id += 1

                let pokemon = Pokemon(context: privateContext)
                pokemon.name = JSONPokemon.name
                pokemon.url = JSONPokemon.detailUrl
                pokemon.id = id

            }

            try? privateContext.save()
            try? privateContext.parent?.save()
            completion()
        } catch let err {
            print("Unable to decode PokemonJSON. Error: ",err)
            completion()
        }
    }.resume()
}

private var detailTracker = 0
func fetchMoreDetails(objectID: NSManagedObjectID) {

    guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }

    print(pokemon.name)
    print()

    guard let url = URL(string: urlString) else { return }

    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let err = error {
            print("Unable to get more details for pokemon", err)
        }

        guard let data = data else { return }
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase

        do {
            let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
            pokemonDetailJSON.types.forEach { (nestedType) in

                let type = Type(context: privateContext)
                type.name = nestedType.type.name
                type.addToPokemons(pokemon)

            }

            try? privateContext.save()
            try? privateContext.parent?.save()

        } catch let err {
            print("Unable to decode pokemon more details", err)
        }

    }.resume()
}

private var imageTracker = 0
func getPokemonImage(objectID: NSManagedObjectID) {
    guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon else { return }

    let id = String(format: "%03d", pokemon.id)
    let urlString = "https://assets.pokemon.com/assets/cms2/img/pokedex/full/\(id).png"
    print(urlString)



    guard let url = URL(string: urlString) else { return }
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let err = error {
            print("Unable to load image from session.", err)
        }

        guard let data = data else { return }
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
        pokemon.image = data

        self.imageTracker += 1

        if self.imageTracker == self.numberOfPokemons {
            try? privateContext.save()
            try? privateContext.parent?.save()
        }
    }.resume()
}
}

I have 3 entities, which are Pokemon, Type & Ability. I am not doing nothing with ability right now, so we can just ignore that. The first func downloadPokemonFromServer just grabs the first 151 pokemon, saves the name and a url of pokemon. I then use that url to go into another URLSession and grab more information about that pokemon. Which is what the fetchMoreDetails func does. However, this func crashes my app. I don't know what I am doing wrong here, it crashes when I try to save it.

The third func getPokemonImage I go into another URLSession, get the data and save it to my pokemon image attribute. The thing is this works perfectly fine. It saves to my CoreData and it doesn't crash my app.

This is how I call it in my ViewController.

@objc func handleRefresh() {

    if pokemonController.fetchedObjects?.count == 0 {
        Service.shared.downloadPokemonsFromServer {
            let pokemons = self.pokemonController.fetchedObjects
            pokemons?.forEach({ (pokemon) in

               Service.shared.getPokemonImage(objectID: pokemon.objectID)
               //If I uncomment the line below it will crash my app.
               //Service.shared.fetchMoreDetails(objectID: pokemon.objectID)
            })
        }
    }
     tableView.refreshControl?.endRefreshing()
}

Will someone pls help me figure out what I am doing wrong. Would really appreciate the help.

Upvotes: 1

Views: 66

Answers (2)

Tziki
Tziki

Reputation: 330

You need to make sure you're doing all the Core Data work on the same thread as the private context you've created. To do so please use:

privateContext.perform {
   //Core data work: create new entities, connections, delete, edit and more...
}

This can prevent you a lot of headaches and troubles down the road

Upvotes: 1

pbasdf
pbasdf

Reputation: 21536

I think the problem is that you are trying to set a relationship between two objects from different contexts. Your pokemon object is registered with the view context:

    guard let pokemon = CoreDataManager.shared.persistentContainer.viewContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }

whereas your type object is registered with the private context:

    let type = Type(context: privateContext)
    type.name = nestedType.type.name

so this line will not work:

    type.addToPokemons(pokemon)

I would try amending the code to use only the privateContext, something like this:

func fetchMoreDetails(objectID: NSManagedObjectID) {
    let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext    
    guard let pokemon = privateContext.object(with: objectID) as? Pokemon, let urlString = pokemon.url else { return }

    print(pokemon.name)
    print()

    guard let url = URL(string: urlString) else { return }

    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let err = error {
            print("Unable to get more details for pokemon", err)
        }

        guard let data = data else { return }


        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase

        do {
            let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data)
            pokemonDetailJSON.types.forEach { (nestedType) in

                let type = Type(context: privateContext)
                type.name = nestedType.type.name
                type.addToPokemons(pokemon)

            }

            try? privateContext.save()
            try? privateContext.parent?.save()

        } catch let err {
            print("Unable to decode pokemon more details", err)
        }

    }.resume()
}

Upvotes: 0

Related Questions