How to parse json data that provides another url with more data - ios

I'm a little confused how would I parse a json API that gives me 20 objects but then gives me a key of "next" having a url that gives me another 20 objects. I'm using this Pokemon API. It gives me 4 keys: count, previous, results and next. I'm trying to display them all in a collection view but not all at the same time. I would like to load more when the collection view is scrolling down.
I'm just trying to get the name at the moment. This is how my code looks like.
I get it to load the first 20 Pokemon in the collection view. However I don't know how to load the next 20 Pokemon or the 20 after. This is how the json file looks like if the link didn't work.
I would appreciate any help given. :)

You can try using a recursive function reusing the loadPokemonsData function something like this:
func loadPokemonsData(url: String, quantity: Int?) {
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
}
do {
let jsonResults = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
let pokemonArray = jsonResults.value(forKey: "results") as! [[String: Any]]
var isPokemonsEqualsToQuantity: Bool = false
for pokemonData in pokemonArray {
if let quantity = quantity {
guard self.pokemons.count < quantity else {
isPokemonsEqualsToQuantity = true
break
}
}
guard let name = pokemonData["name"] as? String else {
return
}
self.pokemon = Pokemon(name: name)
self.pokemons.append(self.pokemon)
}
guard let nextURL = jsonResults.value(forKey: "next") as? String, !isPokemonsEqualsToQuantity else {
for pokemon in self.pokemons {
print(pokemon.name)
}
print(self.pokemons.count)
return
}
self.loadPokemonsData(url: nextURL, quantity: quantity)
} catch let err as NSError {
print(err.localizedDescription)
}
}
task.resume()
}
Attach a screen of algorithm function running... it prints 791 pokemons.
Hope it helps you!
EDITED
Next time you ask put your code please... it will be easier help you!.
I've updated the code to set the quantity you want (nil if you want to get all pokemons), Therefore it will only get the pokemons in the order API returns it, if you want a specific pokemons from ALL pokemons you may do a sort after obtaining all pokemons.

Related

Swift 5 : Escaping closure captures 'inout' parameter

I already have the response data that I received from the server. This response data have some bakers data.
Now I want to calculate the distance of the user and bakery and then store it in the same modal class. I have created a function for it. And as this function need to be used in 4,5 view controllers, my plan is to create as an extension of UIViewController
func getDistanceUserBakery(bakeryData : inout [BakeryRecord], completion : #escaping (Int?) -> () ) {
for index in 0...(bakeryData.count-1) {
//1
let googleApiAdd = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&"
//2
let origin = "origins=\(UserLocation.coordinates.latitude),\(UserLocation.coordinates.longitude)"
//3
let destination = "&destinations=\(bakeryData[index].location?.coordinates?[1] ?? 0.0),\(bakeryData[index].location?.coordinates?[0] ?? 0.0)"
//4
let googleKey = "&key=\(GOOGLE_KEY)"
//5
let url = googleApiAdd + origin + destination + googleKey
let request = URLRequest(url: URL(string: url)!)
//6 - this line is showing the error.
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
guard let data = data else {
completion(nil)
Toast.show(message: "Unable to calculate distance from user to bakery", controller: self)
return }
let stringResponse = String(data: data, encoding: .utf8)!
let dictData = stringResponse.convertToDictionary()
do {
let jsonData = try JSONSerialization.data(withJSONObject: dictData as Any, options: .prettyPrinted)
let decoder = JSONDecoder()
let model = try decoder.decode(GoogleDistance.self, from: jsonData)
bakeryData[index].disanceInMiles = model.rows?[0].elements?[0].distance?.text ?? "NaN"
completion(index)
} catch let parsingError {
print("Error data :", parsingError)
completion(nil)
}
}
task.resume()
}
This is how I call this function once I have received the data from my server,
self.getDistanceUserBakery(bakeryData: &self.bakeryData) { index in
if index != nil {
DispatchQueue.main.async {
// here I am thinking as the bakeryData will hold the new value for distanceInMiles, the collectionView will start showing up that result on reload.
self.resultCollection.reloadItems(at: [IndexPath(item: index!, section: 0)])
}
}
}
Now the Question:
As I know, when you pass parameters as inout, there values can be changed from inside your function, and those changes reflect in the original value outside the function.
But when I try the code , it says Escaping closure captures 'inout' parameter 'bakeryData'. In my code , //6 is producing the error.
How to fix this error?
As #Paulw11 suggested in comments,
Is BakeryData a struct? If so then simply make it a class. If you make
BakerData a class then the array contains reference types and you can
update the element's properties
I changed the struct to class and it did work.

Property is coming up empty when called from another class when using URLSession

For some reason, the products array is coming back empty when I try and access it from another class. What am I doing wrong, and how can I get the products array to populate? Is it something related to the do/catch?
The print statement shown will give me what I'm looking for, but when I try and use the property in another class after the retrieve method has been called, it comes up empty.
For information, "Product" is a struct that has name, description, etc properties attached.
private let productListUrl = URL(string: "https://api/products.json")
var products = [Product]()
func retrieveProductList() {
if let productListUrl = productListUrl {
URLSession.shared.dataTask(with: productListUrl) { (data, response, error) in
if let data = data {
do {
let jsonData = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
let tempArray: Array = jsonData["products"] as! [Any]
for product in tempArray {
let newProduct = Product(json: product as! [String : Any])
self.products.append(newProduct!)
}
print("In ProductService: \(self.products)")
}
catch {
print("An error occured while attempting to read data")
}
}
}.resume()
}
}
As maddy noted, this is because the URL call is asynchronous.
You basically have 3 options:
Use a semaphore approach and make your retrieveProductList method synchronous.
Change your class to have a delegate property that you can ping when the URL request finishes.
Add a completion handler to your retrieveProductList method that is called when the URL request finishes.
I personally would lean towards option 3:
func retrieveProductList(completion: #escaping ([Product])->())
{
// Right after you print the products...
completion(self.products)
}

How does many to many relationships work.

Okay I have read apple Core Data Guide. I also look on the web but only found posts about 2 years ago. So I don't know how many to many relationship work. I'm building an app using core data. The app has 3 entities Pokemon, Type, Ability. A Pokemon can have 1 or more type so I set the relationship to to-many. A type can have multiple Pokemon associated with it, so I also should set the relationship to to-many but I don't understand many-to-many relationship. Same goes for the abilities. This is how my data model looks like. There are another 2 entities but I don't really care about those right now.
This is the function I'm using to parse the following API. It grabs the first 20 Pokemon save the name and uses the url to fetch more information about that Pokemon. So I added another task that adds the type and ability to that Pokemon.
private func loadPokemon(url: String) {
let context = coreData.persistentContainer.viewContext
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
}
do {
let jsonResults = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
let pokemonArray = jsonResults.value(forKey: "results") as! [[String: Any]]
for pokemonData in pokemonArray {
guard let name = pokemonData["name"] as? String else {
return
}
guard let pokemonInfoURL = pokemonData["url"] as? String else {
return
}
let pokemon = Pokemon(context: context)
pokemon.name = name
print(1)
self.pokemonMoreInfo(for: pokemon, url: pokemonInfoURL, context: context)
}
}
catch let err {
print(err.localizedDescription)
}
}
task.resume()
}
private func pokemonMoreInfo(for pokemon: Pokemon, url: String, context: NSManagedObjectContext) {
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
}
do {
let jsonResults = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
//MARK: Pokemon Abilities
if let abilityArray = jsonResults.value(forKey: "abilities") as? [[String: Any]] {
let abilities = pokemon.ability?.mutableCopy() as! NSMutableSet
for abilityData in abilityArray {
guard let abilityDic = abilityData["ability"] as? NSDictionary else {
return
}
let name = abilityDic.value(forKey: "name") as! String
guard let isHidden = abilityData["is_hidden"] as? Bool else {
return
}
guard let slot = abilityData["slot"] as? Int16 else {
return
}
let ability = Ability(context: context)
ability.name = name
ability.isHidden = isHidden
ability.slot = slot
abilities.add(ability)
pokemon.addToAbility(abilities)
}
}
//MARK: Pokemon Type
if let typeArray = jsonResults.value(forKey: "types") as? [[String: Any]] {
let types = pokemon.type?.mutableCopy() as! NSMutableSet
for typeData in typeArray {
guard let typeDic = typeData["type"] as? NSDictionary else {
return
}
let name = typeDic.value(forKey: "name") as! String
guard let slot = typeData["slot"] as? Int16 else {
return
}
let type = Type(context: context)
type.name = name
type.slot = slot
types.add(type)
pokemon.addToType(types)
}
}
}
catch let err {
print(err.localizedDescription)
}
self.coreData.saveContext()
}
task.resume()
}
I'm using this app called SQLight Read-Only. The ability and type are matching to the correct Pokemon. These are screenshots on how my SQLight looks like.
I'm not sure if you guys know about Pokemon, but charizard type is fire and flying and have the abilities of solar-power and blaze. So I know that I'm saving the data correctly. However my SQLight have the same type repeating like fire, grass, poison same goes for the abilities but with the correct Pokemon associated with them. This is how my complete SQLight looks like.
Not sure if it will keep repeating with a many-to-many relationship. So my question is how would I use a many-to-many relationship with Pokemon to type and ability. So how would I add a Pokemon with the same type or same abilities. So later, I can perform a fetch that grabs all Pokemon that have a type of fire or same ability. I'm not sure if I explained my question correctly might be a little confusing.
Would appreciate any help. :)
EDIT:
Actually what I wrote below isn't correct for Core Data. (Thanks for pointing that out in the comments Paulw11.) From the Core Data Guide:
Many-to-Many Relationships
You define a many-to-many relationship using two to-many relationships. The first to-many relationship goes from the first entity (the source entity) to the second entity (the destination). The second to-many relationship goes from the second entity (the original destination entity) to the first entity (the original source entity). You then set each to be the inverse of the other. (If you have a background in database management and this causes you concern, don't worry: if you use an SQLite store, Core Data automatically creates the intermediate join table for you.)
Old answer (incorrect for Core Data):
To create a many-to-many relationship in a relational database you have to add a helper table. In your case you could call it PokemonWithType. It has two columns, one for the Pokemon id and another for the Type id. If you want to find all Types for one specific Pokemon you just query all entries in PokemonWithType with the specific Pokemon id. If you want to find all Pokemon that have a specific Type you do the same just with the Type id.

Images loading in incorrectly even with cache

if let toID = message.chatPartnerId() {
firebaseReference.child(toID).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
cell.nameLabel.text = dictionary["displayname"] as? String
let pic = dictionary["pictureURL"] as! String
print("THIS IS THE URL FOR EACH DISPLAYNAME")
print(dictionary["displayname"] as? String)
print(pic)
if let imageFromCache = MainPageVC.imageCache.object(forKey: pic as NSString) {
cell.pictureLabel.image = imageFromCache
} else {
let requested = URLRequest(url: URL(string: pic )!)
URLSession.shared.dataTask(with: requested) {data, response, err in
if err != nil {
print(err)
} else {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
MainPageVC.imageCache.setObject(imageToCache!, forKey: pic as NSString)
//cell.pictureLabel.image = nil
cell.pictureLabel.image = imageToCache
}
}
}.resume()
}
}
})
}
return cell
}
I'm running this code in my cellForRowAtIndexPath and I'm getting a ton of really bad behavior. I'm also getting similar behavior on other pages but for some reason this block of code with about a 90% consistency returns incorrect information for cells.
I get a lot of duplicate pictures being used, displaynames in the wrong places, but when I'm actually clicking into a person, my detail page shows the correct information every single time. That code is the typical didSelectRowAtIndexPath and passing the person.
What I don't understand is why on the initial load of this page all of the information is screwed up, but if I click into someone and come back the entire tableview has correct names and pictures. The names/pics also fix if I scroll a cell off the screen then come back to it.
I'm getting this behavior all over my app, meanwhile I see caching/loading done like this everywhere. Is it because I'm running the code in my cellForRowAtIndexPath? The only difference I see is that I'm running it there instead of creating a function inside of my Person class that configures cells and running it like that. What I don't understand is why that would make a difference because as far as I'm aware running a function within cellforRowAtIndexpath would be the same as copy-pasting that same code into there?
Any ideas/suggestions?
Edit: I'm getting a very similar situation when I'm running the following code:
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
self.tableView.reloadData()
Where I'm sorting my array before reloading my data. The information sometimes loads in incorrectly at first, but once I scroll the cell off the screen then come back to it it always corrects itself.
if you are using swift 3 here are some handy functions that allow you to save an image to your apps directory from an URL and then access it from anywhere in the app:
func saveCurrentUserImage(toDirectory urlString:String?) {
if urlString != nil {
let imgURL: URL = URL(string: urlString!)!
let request: URLRequest = URLRequest(url: imgURL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if (error == nil && data != nil) {
func display_image() {
let userImage = UIImage(data: data!)
if let userImageData = UIImagePNGRepresentation(userImage!) {
let filename = self.getDocumentsDirectory().appendingPathComponent("userImage")
try? userImageData.write(to: URL(fileURLWithPath: filename), options: [.atomic])
}
}
DispatchQueue.main.async(execute: display_image)
}
})
task.resume()
}
}
and then access it with any view controller using this:
extension UIViewController {
func getImage(withName name: String) -> UIImage {
let readPath = getDocumentsDirectory().appendingPathComponent(name)
let image = UIImage(contentsOfFile: readPath)
return image!
}
}
and finally calling it like this:
cell.pictureLabel.image = getImage(withName: "userImage")
If you can run the saveCurrentUserImage function prior to running cellForRowAtIndexPath then you can just check if the photo is nil in the directory before attempting to download it. You might be getting funny behavior when the page initially loads because you have multiple network calls going on at once. I wouldn't recommend making any network calls in cellForRowAtIndexPath because every time the cells are re-initialized it's going to make that network call for each cell.
Hope it helps!
EDIT: This method of image saving and retrieval is for images that you want to persist. If you want to erase them from memory you'll have to delete them from your directory.

Display array after action

I have a little app that downloads some names from the web, and then appends them to an array.
func fetchTitle(identifier: String, completion: (title: String) -> Void) {
let profileUrl = NSURL(string:"http://www.facebook.com/" + identifier)!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
let title = secondArray[0]
completion(title: title)
print(title)
}
}
task.resume()
}
//print(newArray)
var titles = [String]()
//let identifiers = ["100001986741004","100003866283798","100003455181526"]
let queue = dispatch_queue_create("titles", DISPATCH_QUEUE_SERIAL)
dispatch_apply(newArray.count, queue) { index in
let identifier = newArray[index]
fetchTitle(identifier) { title in
dispatch_async(queue) {
titles.append(title)
array.append(title)
}
}
}
I know it's pretty complicated, because it takes numbers from an array and turns them into names downloaded from the web, but never mind about that. The problem is, when I print title, it gives me the names, so I assume it does append them to the array, but when I print the array, it gives me no result.. I think this is because it takes a little while to download the data from the web, and the print happens immediately, but how to I delay the print (or display into table view) until the download is complete?
Any help is appreciated!
Thank you very much!
I don't know about swift, but when your data has been retrieved, in Objective-C you can call [tableView reloadData]; should be simple enough to translate to Swift

Resources