How to add to an NSSet using Core Data in Swift 5 - swift

So I'm practicing a little more with core data after finishing a course. So I am still a little new to it. So I Have 3 entities named Pokemon, Type & Ability. So a Pokemon can have many types like Fire,Water,Flying and so on. Type can also have multiple Pokemon that are Fire,Water,Flying and so on. Same goes for the Ability, so I made a many-to-many relationship. Here is how it looks like.
I am parsing some JSON form an api and trying to save it into core data. Now here is where I am having a bit of trouble. This is how my code looks and it just basically parse the JSON.
struct Service {
static let shared = Service()
func downloadPokemonsFromServer(completion: #escaping ()->()) {
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=9"
guard let url = URL(string: urlString) else { return }
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
let pokemon = Pokemon(context: privateContext)
pokemon.name = JSONPokemon.name
pokemon.url = JSONPokemon.detailUrl
//Would want to set pokemon types here but
//When i call fetchMoreDetails(pokemon:,urlString:,completion:)
//The pokemon is always nil inside fetchMoreDetails
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode PokemonJSON. Error: ",err)
completion()
}
}.resume()
}
func fetchMoreDetails(pokemon: Pokemon, urlString: String, completion: #escaping ()->()) {
guard let url = URL(string: urlString) else { return }
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = CoreDataManager.shared.persistentContainer.viewContext
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
//How do I add type to pokemon.types this does work
//pokemon.types?.adding(type)
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode pokemon more details", err)
completion()
}
}.resume()
}
}
I am able to parse everything fine and all but I just can't seem to add a new type to pokemons.types. I have look on stack overflow but most of the solutions seem to be in Objective C.
This is how my ViewController looks like and I am also using a NSFetchResultController.
class PokemonTableVC: UITableViewController {
lazy var pokemonController: NSFetchedResultsController<Pokemon> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
let nameSort = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [nameSort]
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = self
return controller
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
tableView.refreshControl = refreshControl
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Delete", style: .done, target: self, action: #selector(handleDelete))
try? pokemonController.performFetch()
}
#objc func handleDelete() {
print("Deleting")
let context = CoreDataManager.shared.persistentContainer.viewContext
guard let pokemons = pokemonController.fetchedObjects else { return }
pokemons.forEach { (pokemon) in
context.delete(pokemon)
}
do {
try context.save()
} catch let err {
print("Unable to save data", err)
}
}
#objc func handleRefresh() {
print("DDDDD")
Service.shared.downloadPokemonsFromServer {
self.pokemonController.fetchedObjects?.forEach({ (pokemon) in
print(pokemon.name)
Service.shared.fetchMoreDetails(pokemon: pokemon, urlString: pokemon.url ?? "") {
print(pokemon.abilities?.count)
}
})
}
tableView.refreshControl?.endRefreshing()
}
}
I can provide my other structs if needed. But basically I am trying to add a type to pokemon.types would also like to add fetchMoreDetails when I fetch pokemons where I put the comment at. Would
really appreciate any feedback.

When you add a relationship to an entity Xcode creates methods for getting and setting values for that relationship using a pre-defined naming standard. So you should have some methods in your Pokemon class for setting Type instances (and code completion should be able to help here):
addToTypes(value:) // single object
addToTypes(values:) //set of objects
So in your code it should be
pokemon.addToTypes(value: type)
You also have the same methods on Type for the opposite direction

Related

Can't load JSON content because of GCD

App is project number 7 from Hacking with swift "Showing some JSON".
Because i have to filtrate through the results i have to made two arrays that store same JSON data.
var petitions = [Petition]()
var filtrated = [Petition] ()
There is a purple warning problem when i added DispatchQueue.global(qos: .userInitiated).async{ to viewDidLoad()
class ViewController: UITableViewController {
var petitions = [Petition]()
var filtrated = [Petition] ()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem (title: "Filter", style: .plain, target: self, action: #selector(resenje))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refresh))// ili samo #selector(webView.reload)
let urlString : String
if navigationController?.tabBarItem.tag == 0 {
urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}
DispatchQueue.global(qos: .userInitiated).async{ [self] in
if let url = URL(string: urlString){
if let data = try? Data(contentsOf: url){
parse(json: data)
filtrated = petitions
return
}
}
func parse(json: Data) {
let decoder = JSONDecoder()
if let jsonPetitions = try? decoder.decode(Petitions.self, from: json){
petitions = jsonPetitions.results
tableView?.reloadData() //error:UITableView.reloadData() must be used from main thread only
Program will work but it shows error alert I made and tableView is empty
The GCD global queue runs on a background thread. But all UI updates must take place on the main thread. As an aside, you also do not want a background thread to update properties, such as filtrated. So you will want to dispatch both the model and UI updates back to the main thread.
So, fetch and parse the data, and then dispatch the updates to the main queue:
DispatchQueue.global(qos: .userInitiated).async { [self] in
guard
let url = URL(string: urlString),
let data = try? Data(contentsOf: url),
let responseObject = try? JSONDecoder().decode(Petitions.self, from: data)
else {
return
}
DispatchQueue.main.async {
filtrated = responseObject.results
tableView?.reloadData()
}
}
Or, better, use URLSession. And do not use try?, because if it fails, you are throwing away the Error object that provides crucial diagnostic information:
guard let url = URL(string: urlString) else {
print("Invalid url: \(urlString)")
return
}
let task = URLSession.shared.dataTask(with: url) { [self] data, response, error in
guard let data = data else {
print("Request failed:", error ?? "Unknown error")
return
}
do {
let responseObject = try JSONDecoder().decode(Petitions.self, from: data)
DispatchQueue.main.async {
filtrated = responseObject.results
tableView?.reloadData()
}
} catch let parsingError {
print("Parsing failed:", parsingError)
return
}
}
task.resume()
You need to head back to the main thread to update the UI
DispatchQueue.main.async{
self.tableView?.reloadData()
}

Error when working with NSManagedObjectContext in background thread

So I keep getting this error when I save to core data.
Coredata[21468:13173906] [error] error: SQLCore dispatchRequest: exception handling request: , ** -_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]! with userInfo of (null)
I recently finished a course on core data, the instructor brush a bit on the topic that core data isn't thread safe. So what he suggested is a child/parent context, so that is what I tried doing but kept getting the error from above. This is how my code looks.
struct Service {
static let shared = Service()
func downloadPokemonsFromServer(completion: #escaping ()->()) {
let urlString = "https://pokeapi.co/api/v2/pokemon?limit=9"
guard let url = URL(string: urlString) else { return }
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
//Works fine with privateContext here
let pokemon = Pokemon(context: privateContext)
pokemon.name = JSONPokemon.name
pokemon.url = JSONPokemon.detailUrl
}
//Why does the privateContext work here
//and doesn't crash my app.
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode PokemonJSON. Error: ",err)
completion()
}
}.resume()
}
func fetchMoreDetails(pokemon: Pokemon, urlString: String, context: NSManagedObjectContext, completion: #escaping ()->()) {
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
// I can just change this to context and it works
// Why doesnt it work with privateContext and crashes my app
let type = Type(context: privateContext)
type.name = nestedType.type.name
pokemon.addToTypes(type)
}
try privateContext.save()
try privateContext.parent?.save()
completion()
} catch let err {
print("Unable to decode pokemon more details", err)
completion()
}
}.resume()
}
}
So in the downloadPokemonsFromSever, I make a call to an api that parse the json and saves it into Coredata. I followed my instructor instructions, by creating a privateContext and then setting its parent to my mainContext. Then I create a new Pokemon that has a name & url using my privateContext and NOT my mainContext. When I completely parse my Pokemon I go into another api that has more details on that Pokemon.
This is where my app starts to crash. As you can see from the fetchMoreDetails there is a parameter that is context. When I try to create a new Type with privateContext it crashes my app. When I use the context that is passed through it works fine. I would like to know why privateContext works inside downloadPokemonFromServer and not in fetchMoreDetails. I left a comment above the line that I think that crashes my app. This is how I call it in my ViewController, using this action.
#objc func handleRefresh() {
Service.shared.downloadPokemonsFromServer {
let context = CoreDataManager.shared.persistentContainer.viewContext
self.pokemonController.fetchedObjects?.forEach({ (pokemon) in
Service.shared.fetchMoreDetails(pokemon: pokemon, urlString: pokemon.url ?? "", context: context) {
}
})
}
tableView.refreshControl?.endRefreshing()
}

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.
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.
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
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()
}

Return response as object in swift

I have a function that connects to an API to retrieve data. The API takes two parameters accessCode (provided by user in a text box) and then UDID (UDID of their device). I can parse the data from within the function, but only locally. I need to store the values that are returned but am unsure on how to return them properly. Essentially I need this to return the json object as a dictionary (I think...) so it can be parsed outside of the async task. I've read through the swift documentation and that's where I found out how to do the requests, but I can't find a way to store the returned values in memory for access outside of the function.
func getResponse(accessCode:String, UDID:String, _ completion: #escaping (NSDictionary) -> ()) {
let urlPath = "https://apihosthere.com/api/validate?accessCode=" + accessCode + "&UDID=" + UDID
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult as? NSDictionary
print(results)
completion(results!)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
First of all don't use NSDictionary in Swift, use native [String:Any] and declare the type as optional to return nil if an error occurs.
And never use .mutableContainers in Swift, the option is useless.
func getResponse(accessCode:String, UDID:String, completion: #escaping ([String:Any]?) -> Void)) {
let urlPath = "https://apihosthere.com/api/validate?accessCode=" + accessCode + "&UDID=" + UDID
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error else {
print(error)
completion(nil)
return
}
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
print(jsonResult)
completion(jsonResult)
} else {
completion(nil)
}
} catch {
print(error)
completion(nil)
}
}
task.resume()
}
Your mistake is that you don't consider the closure, you have to execute the entire code inside the completion handler
#IBAction func StartWizard(_ sender: UIButton) {
//Store entered access code
let accessCode = AccessCodeField.text!
//Call API to validate Access Code
getResponse(accessCode:accessCode, UDID:myDeviceUDID) { [weak self] result in
if let accessCodeFound = result?["Found"] as? Bool {
print("Value of Found during function:")
//If access code is valid, go to License view
print(accessCodeFound)
if accessCodeFound {
//Load License View
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let licenseController = storyboard.instantiateViewController(identifier: "LicenseViewPanel")
self?.show(licenseController, sender: self)
}
}
}
}
}
Your completion closure should handle the obtained data. You would call the function like this:
getResponse(accessCode: "code", UDID: "udid", completion: { result in
// Do whatever you need to do with the dictionary result
}
Also, I'd recommend you to change your NSDictionary with a swift Dictionary.
This is what the API returns as a response
{
AccessCode = 00000000;
Client = "0000 - My Company Name";
EmailAddress = "brandon#brandonthomas.me";
FirstName = Brandon;
Found = 1;
LastName = Thomas;
Status = A;
UDIDregistered = 1;
}
And this is what calls the function. I am calling at after clicking a button after an access code is being entered in a text field.
#IBAction func StartWizard(_ sender: UIButton) {
//Store entered access code
let accessCode = AccessCodeField.text!
var accessCodeFound: Bool? = nil
//Call API to validate Access Code
getResponse(accessCode:accessCode, UDID:myDeviceUDID) { result in
accessCodeFound = result["Found"] as! Bool
print("Value of Found during function:")
print(accessCodeFound)
//accessCodeFound = true
}
//If access code is valid, go to License view
print("Value of Found after function:")
print(accessCodeFound)
//accessCodeFound = nil ???
//it seems the value is getting reset after the function completes
if accessCodeFound == true{
//Load License View
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let licenseController = storyboard.instantiateViewController(identifier: "LicenseViewPanel")
self.show(licenseController, sender: Any?.self)
}
}

Data tasks outside ViewController

I'm gonna start with I'm currently learning swift + iOS so I'm by no means an experienced developer or one for that matter.
My goal is to separate any network calls that are currently done in my view controller to a dedicated class outside of it.
In this view controller i have a IBAction with the following code inside of it:
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
What I want, or I think I want to achieve is something like this:
let apiData = "somehow get it from outside"
Then when apiData has info stored in it, execute this next bit of code:
let token = apiData.data?.token
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
How would I achieve this? Thank you.
You can try
class API {
static func userLoginWith(email:String,password:String,completion:#escaping(_ token:String?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: loginRequest) {
(data, response, error) in
guard let _ = response, let data = data else { completion(nil) ; return }
do {
let apiData = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(apiData.data?.token)
} catch {
print("Decoder error: ",error")
completion(nil)
}
}.resume()
}
}
Inside the VC
API.userLoginWith(email:<##>,password:<##>) { (token) in
if let token = token {
let saveToken: Bool = KeychainWrapper.standard.set(token!, forKey: "token")
DispatchQueue.main.async {
self.showOrHideActivityIndicator(showOrHide: false)
self.showHomeScreen()
}
}
}