Swift 5中Core Data向NSSet添加数据的问题(宝可梦场景)
Let's break down your two Core Data issues and fix them step by step:
1. Pokemon Instance is Nil in fetchMoreDetails
Root Cause
The main problem here is Core Data thread safety violations:
- You're passing a
Pokemoninstance from the main queue's view context to a background network thread infetchMoreDetails. Core Data managed objects are bound to their context's queue—accessing them outside that queue causes them to become invalid (appear asnilor trigger faults). - There's also a potential context sync timing issue: after saving the private context in
downloadPokemonsFromServer, the view context's fetched results controller might not immediately update, leading you to access stale objects.
Fix
Instead of passing the Pokemon instance directly, pass its thread-safe objectID. Then fetch the corresponding object in the private context of fetchMoreDetails, and always perform Core Data operations within the context's queue using perform:
Updated fetchMoreDetails Method
func fetchMoreDetails(pokemonObjectID: NSManagedObjectID, urlString: String, completion: @escaping ()->()) { guard let url = URL(string: urlString) else { completion() return } let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext // Perform all Core Data work on the private context's queue privateContext.perform { URLSession.shared.dataTask(with: url) { (data, response, error) in if let err = error { print("Unable to get more details for pokemon", err) completion() return } guard let data = data else { completion() return } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let pokemonDetailJSON = try decoder.decode(PokemonDetailJSON.self, from: data) // Fetch the Pokemon instance using the objectID in the private context guard let pokemon = privateContext.object(with: pokemonObjectID) as? Pokemon else { print("Failed to fetch Pokemon from objectID") completion() return } pokemonDetailJSON.types.forEach { (nestedType) in let type = Type(context: privateContext) type.name = nestedType.type.name // Add type to Pokemon's types relation (fix for issue 2) let mutableTypes = pokemon.mutableSetValue(forKey: "types") mutableTypes.add(type) } try privateContext.save() // Save parent context on its own queue privateContext.parent?.perform { try? privateContext.parent?.save() completion() } } catch let err { print("Unable to decode pokemon more details", err) completion() } }.resume() } }
Updated handleRefresh in ViewController
@objc func handleRefresh() { print("DDDDD") Service.shared.downloadPokemonsFromServer { // Refresh fetched results to get latest data after save do { try self.pokemonController.performFetch() } catch let err { print("Failed to refetch pokemons: \(err)") } self.pokemonController.fetchedObjects?.forEach({ (pokemon) in print(pokemon.name) // Pass objectID instead of the managed object instance Service.shared.fetchMoreDetails(pokemonObjectID: pokemon.objectID, urlString: pokemon.url ?? "") { print(pokemon.abilities?.count) } }) self.tableView.refreshControl?.endRefreshing() } }
2. Can't Add Type to pokemon.types NSSet
Root Cause
NSSet is an immutable collection—calling pokemon.types?.adding(type) creates a new set but doesn't update the pokemon.types property. Additionally, you need to ensure the operation happens on the correct context's queue.
Fix
Use Core Data's built-in mutableSetValue(forKey:) method to get a mutable collection for the relation, then add your Type instance directly. This handles the Core Data relation management properly:
// Inside fetchMoreDetails, after creating the Type instance: let mutableTypes = pokemon.mutableSetValue(forKey: "types") mutableTypes.add(type)
This modifies the underlying relation directly, and changes will be saved when you persist the private context.
Additional Best Practices
- Always wrap Core Data operations in
context.perform { ... }when working with background contexts to ensure thread safety. - When saving a child context, save the parent context within its own queue using
parent?.perform { ... }to avoid cross-thread issues. - For fetched results controllers, call
performFetch()after context saves to ensure the UI reflects the latest data.
内容的提问来源于stack exchange,提问作者Luis Ramirez

