Swift - Troubles with API calls / CompletionHandler / URLSessions - ios

I have this long function here which makes a bunch of API calls, parses through data and returns two arrays representing a bunch of sight-seeing locations (one array holds the latitudes, one holds the latitudes). The issue I am having is determining when the two arrays are finished being populated. Ideally, I would like to be able to place
print("ArrayCount = \(self.latArray.count)")
somewhere in my code and receive a single print statement in the console reading ArrayCount = 123. However everywhere I place the print statement I get either an array count of 0 or a loop of values being printed out as they are added (1..2..3.. ... ..123). Thanks in advance!
func someFunction()
{
let url:URL = URL(string: "...")
let task = URLSession.shared.dataTask(with: URLRequest(url: url))
{
data, response, error in
if error != nil
{
print("ERROR IN API REQUEST: \(error!.localizedDescription)")
}
else
{
do
{
if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
if let layerOne = parsedData["one"] as? [String: Any]
{
if let layerTwo = layerOne["two"] as? [[String: Any]]
{
for layerThree in layerTwo
{
if let variableName = layerThree["value"] as? String
{
let innerUrl:URL = URL(string: "...")!
let innerTask = URLSession.shared.dataTask(with: URLRequest(url: innerUrl))
{
data, response, error in
if error != nil
{
print("ERROR IN API REQUEST: \(error!.localizedDescription)")
}
else
{
do
{
if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
if let layerA = parsedData["A"] as? [String: Any]
{
if let lat = layerA["Latitude"] as? String, let long = layerA["Longitude"] as? String
{
self.latArray.append(lat)
self.longArray.append(lon)
}
}
}
}
catch
{
print("ERROR IN JSON SERIALIZATION")
}
}
}
innerTask.resume()
}
}
}
}
}
}
catch
{
print("ERROR IN JSON SERIALIZATION")
}
}
}
task.resume()
}

I'm not sure exactly what you're trying to do, but once I thinned out your incredibly verbose code and tried to understand it, I think the conclusion is that you'll only want to record the array counts after the last iteration of your inner loop. For example:
Instead of doing this: for layerThree in layerTwo
do this: for (index, layerThree) in layerTwo.enumerated()
Then, after you append the lat/long values, add this check:
if index == layerTwo.count - 1 //if this is our last inner loop
{
//print the array counts
print("Lat count: \(latArray.count)")
print("Long count: \(longArray.count)")
}
This should work in your case. However, I disagree entirely with the execution of this simply because of the lack of portability and reusability of your code. Additionally, there are quite a few good language constructs which are being ignored. The relentless nested if let blocks could be reduced significantly with a few good guard statements. Furthermore, considering you aren't handling your errors in any catch blocks anyway, might as well just remove them and opt for a try? instead. One much swiftier way of handling things would be to just include a completion handler in the function itself so that the array count printing logic could be handled elsewhere. I'll include some example code of how I would clean things up:
func someFunction(completion: (([String], [String]) -> Void)?)
{
let url = URL(string: "...")!
var latArray: [String] = []
var longArray: [String] = []
let task = URLSession.shared.dataTask(with: URLRequest(url: url),
completionHandler: { (data, response, error) -> Void in
guard error == nil else
{
print("ERROR IN API REQUEST: \(error?.localizedDescription)")
return
}
guard let parsedData = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments),
let parsedDict = parsedData as? [String: Any],
let layerOne = parsedDict["one"] as? [String: Any],
let layerTwo = layerOne["two"] as? [[String: Any]] else
{
print("JSON OBJECT DOES NOT MATCH EXPECTED PATTERN")
return
}
for (index, layerThree) in layerTwo.enumerated()
{
guard let _ = layerThree["value"] as? String else
{
print("Skip this iteration (I guess?)")
continue
}
let innerUrl = URL(string: "...")!
let innerTask = URLSession.shared.dataTask(with: URLRequest(url: innerUrl),
completionHandler: { (data, response, error) -> Void in
guard error == nil else
{
print("ERROR IN API REQUEST: \(error?.localizedDescription)")
return
}
guard let parsedData = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments),
let parsedDict = parsedData as? [String: Any],
let layerA = parsedDict["A"] as? [String: Any],
let lat = layerA["Latitude"] as? String,
let long = layerA["Longitude"] as? String else
{
print("Something went wrong")
return
}
latArray.append(lat)
longArray.append(long)
if index == layerTwo.count - 1 //if this is our last inner loop
{
completion?(latArray, longArray)
}
}
)
innerTask.resume()
}
}
)
task.resume()
}
someFunction(completion: { (latArray, longArray) -> Void in
//print the array counts
print("Lat count: \(latArray.count)")
print("Long count: \(longArray.count)")
})
This could still be improved much further, though based on your question I don't see that I personally should be putting more time into constructing this for you. It would be best if you come up with a few methods of your own to build an application with more readable and reusable code.

Related

Why URLSession.DataTask.shared is not working properly?

I need to fetch some quizzes for my application from the server. Unfortunately, it seems that URLSession.DataTask.shared is not working. How do I fix the problem?
This is for Swift 4.
import Foundation
import UIKit
class QuizService {
let baseUrl = "https://iosquiz.herokuapp.com/api/quizzes"
func fetchQuizzes(completion: #escaping (([Quiz]?) -> Void)) -> Void {
if let url = URL(string: baseUrl) {
let request = URLRequest(url: url)
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
if let resultsList = json as? [String: Any], let results = resultsList["quizzes"] as? [[String: Any]] {
let quizzes = results.map({ json -> Quiz? in
print(json)
if
let title = json["title"] as? String,
let id = json["id"] as? Int,
let level = json["level"] as? Int,
let description = json["description"] as? String,
let category = json["category"] as? String,
let questions = json["questions"] as? [String: Any],
let imageUrl = json["image"] as? String {
let quiz=Quiz(id:id,title:title,descript:description,category:category,level:level,imageUrl:imageUrl,questions:questions)
return quiz
} else {
return nil
}
}).filter { $0 != nil } .map { $0! }
completion(quizzes)
} else {
completion(nil)
}
} catch {
completion(nil)
}
} else {
completion(nil)
}
}
dataTask.resume()
} else {
completion(nil)
}
}
}
My error is that field of quizzes are null, so my code is not working in my view controller.
I took a look at the response here https://iosquiz.herokuapp.com/api/quizzes.
The "questions" should be array of dictionaries instead of dictionary.
so it should works if you replace this line
let questions = json["questions"] as? [String: Any]
with this line
let questions = json["questions"] as? [[String: Any]]
BTW, I prefer to extract the logic of parsing json into another method to keep fetchQuizzes method simple.
Hope this helps!

Json data to label

I have been struggling with this for a while and just need some quick help with this basic question.
let url = URL(string: "https://api.coindesk.com/v1/bpi/currentprice.json")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print ("Error!")
}
else
{
if let content = data
{
do
{
// Array
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
if let rates = myJson["bpi"] as? NSDictionary
{
if let currency = rates["USD"] as NSDictionary
{
if let money = currency["rate"]
{
print(money)
}
}
}
}
catch
{
}
}
}
}
task.resume()
self.label.text = (self.money as String?)
I am trying to pass the numerical value for "money" to a label in my view controller.
The main issue is that you are trying to update the label from outside the completion handler of your asynchronous network request. However, there are several other issues with your code. You shouldn't use NSDictionary in Swift, use [String:Any] when parsing JSON dictionaries. .mutableContainers also doesn't have any effect in Swift. Also make sure that you dispatch any UI related updates to the main thread.
let url = URL(string: "https://api.coindesk.com/v1/bpi/currentprice.json")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data else {
print(error!); return
}
do {
if let myJson = try JSONSerialization.jsonObject(with: content) as? [String:Any],
let rates = myJson["bpi"] as? [String:Any],
let currency = rates["USD"] as? [String:Any],
let money = currency["rate"] as? String {
DispatchQueue.main.async{
self.label.text = money
}
}
} catch{
print(error)
}
}
task.resume()

Multidimensional array/dictionary loop in Swift

I'm trying to loop through a multidimensional array/dictionary in Swift which I got from a JSON string. Originally I come from PHP so I'm probably a bit off with my approach.
My issue is that I can't seem to get into a sub-array/dict.
My code so far:
func getJSON(){
let url = NSURL(string: url)
URLSession.shared.dataTask(with: (url as URL?)!, completionHandler: {(data, response, error) -> Void in
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
print(jsonObj!)
let array = jsonObj!.allKeys
for keys in array {
print(keys)
}
OperationQueue.main.addOperation({
//Nothing to do right now
})
}
}).resume()
}
My JSON:
{
"ArrayName1": {
"info": "This is my first array!",
"more": "Even more info!"
},
"ArrayName2": {
"info": "This is my second array!",
"more": "Even more info about the second array!"
}
}
The function prints the key (e.g. ArrayName1) which is good, but how do I get deeper into the array? To print the "info"?
If you are sure it's dictionary in this form [String: [String: Any]], you may want to try this.
if let jsonObj = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: [String: Any]] {
let array = jsonObj!.allKeys
for key in array {
print(key)
let info = jsonObj[key]["info"] as! String
}
}
First of all don't use NSURL in Swift 3. There is a native struct URL.
Second of all .allowFragments is useless as the JSON is clearly a collection type.
Third of all don't use NSDictionary in Swift, use native type Dictionary.
Fourth of all do the error handling at least the possible error passed by the data task.
All collection types are dictionaries. Use Fast Enumeration to parse keys and values. All keys and values are strings.
func getJSON(){
let url = URL(string: url)!
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) -> Void in
if error != nil {
print(error!)
return
}
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:[String:String]] {
for (key, innerDictionary) in jsonObj {
print(key)
if let info = innerDictionary["info"] {
print("info", info)
}
}
DispatchQueue.main.async {
//Nothing to do right now
}
} else { print("The JSON object is not [String:[String:String]]") }
} catch {
print(error)
}
}).resume()
}

I tried to get the 'location' from the below JSON, but it returns response 'nil'

I tried to get the location from the below JSON, but it returns response nil, can you check it once. Below URL gives the response, but I want to display location from below JSON.
let url = URL(string: "http://beta.json-generator.com/api/json/get/4ytNy-Nv7")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print ("ERROR")
}
else
{
if let content = data
{
do
{
//Array
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
print(myJson)
let val = myJson["Location"] as? NSDictionary
print("val=\(val)")
}
catch
{
}
}
}
}
task.resume()
Don't use Foundation datatypes, such as NSDictionary, when they have native Swift counterparts. You also need to cast the JSON to an array of dictionaries. However, the problem that actually caused the issue was that Location is a String and not a dictionary.
guard let myJsonArray = try JSONSerialization.jsonObject(with: content) as? [[String:Any]], let myJson = myJsonArray.first else {return}
print(myJson)
let val = myJson["Location"] as? String
print("val=\(val)")
The root object of the JSON is clearly an array of a dictionary not something (AnyObject). The value for key Location is in the first object of the array
if let myJson = try JSONSerialization.jsonObject(with: content) as? [[String:Any]], !myJson.isEmpty { // check also that the array is not empty
print(myJson)
let val = myJson[0] // get first object of the array
let location = val["Location"] as? String ?? "n/a"
print("location = \(location)")
}
You can use the following function to download your data. Further more since your array has only one object, to access multiple locations you can iterate through the array objects
func downloadData(){
let url = URL(string: "http://beta.json-generator.com/api/json/get/4ytNy-Nv7")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print ("ERROR")
}
else
{
if let content = data
{
do
{
let myJson = try JSONSerialization.jsonObject(with: content) as? [[String:Any]]
let object = myJson?[0]
if let location = object?["Location"] as? String{
print(location)
}
}
catch
{
}
}
}
}
task.resume()
}
You can change this part :
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
Into :
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as! [AnyHashable: Any]
Then if your JSON only have one object try this to get the Location :
let obj = myJson[0]
let location = obj["Location"] as? String
print("Location \(location)")

how to access array inside json object in swift

Can't access json object which is array inside json object
i want to access data from json object which have array inside array
and that json file is also uploaded
so pls can anyone check and help me how to get "weather.description"
data
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=13ae70c6aefa867c44962edc13f94404")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("some error occured")
} else {
if let urlContent = data {
do{
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers)
let newValue = jsonResult as! NSDictionary
print(jsonResult)
let name = newValue["name"]
//Here i am getting name as variable value
//this is not working
let description = newValue["weather"]??[0]["description"]
//this is not working
let description = newValue["weather"]!![0]["description"]
print()
}catch {
print("JSON Preocessing failed")
}
}
}
}
task.resume()
}
I have edited your code a bit, and added a few comments. Basiclly, lets check for the types of your response structure, and get the desired value.
let url = URL(string: "http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=13ae70c6aefa867c44962edc13f94404")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print("some error occured")
} else {
if let urlContent = data {
do{
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers)
// I would not recommend to use NSDictionary, try using Swift types instead
guard let newValue = jsonResult as? [String: Any] else {
print("invalid format")
return
}
// Check for the weather parameter as an array of dictionaries and than excess the first array's description
if let weather = newValue["weather"] as? [[String: Any]], let description = weather.first?["description"] as? String {
print(description)
}
}catch {
print("JSON Preocessing failed")
}
}
}
}
task.resume()

Resources