Global variable doesn't store any data in anonymous method - ios

trying to get nasa.gov asteroid's data. There is a asteroids global variable of array of Asteroid instances. There is about 1000 occurrences in the jsonData variable. When I append the occurrence at the line self.asteroids.append(), I can see it's adding. When the anonymous completionHandler method ends, variable self.asteroids is empty again, so it doesn't reload no data.
It doesn't make any sense to me since asteroids is a global variable and it should store any data appended to it. Can anyone help?
class ViewController: UITableViewController {
var asteroids = [Asteroid]()
override func viewDidLoad() {
super.viewDidLoad()
let connectionString: String = "https://data.nasa.gov/resource/y77d-th95.json"
let url = NSURL(string: connectionString)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
for index in 0 ... (jsonData.count - 1) {
self.asteroids.append(Asteroid(name: jsonData[index]["name"] as! NSString as String))
}
} catch {
print("Error")
return
}
})
task.resume()
self.tableView.reloadData()
}

Put the table view's reloadData method in the completion block, after the asteroids array has been modified.
Another way would be to reloadData in asteroid didSet method:
var asteroids = [Asteroid]() {
didSet {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
Code of the completion handler is called after the end of scope of viewDidLoad function. Because the dataTaskWithURL is an asynchronous operation.

Is it empty or you're reloading the table view before the dataTask finishes?
Try to move the reloadData inside the completion closure:
override func viewDidLoad() {
super.viewDidLoad()
let connectionString: String = "https://data.nasa.gov/resource/y77d-th95.json"
let url = NSURL(string: connectionString)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
for index in 0 ... (jsonData.count - 1) {
self.asteroids.append(Asteroid(name: jsonData[index]["name"] as! NSString as String))
}
self.tableView.reloadData()
} catch {
print("Error")
return
}
})
task.resume()
}
UPDATE: A second approach, if you're 100% sure you want the tableView to be updated after all data has been downloaded & parsed could be:
override func viewDidLoad() {
super.viewDidLoad()
let connectionString: String = "https://data.nasa.gov/resource/y77d-th95.json"
let url = NSURL(string: connectionString)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions())
for index in 0 ... (jsonData.count - 1) {
self.asteroids.append(Asteroid(name: jsonData[index]["name"] as! NSString as String))
}
self.tableView.delegate = self
self.tableView.dataSource = self
} catch {
print("Error")
return
}
})
task.resume()
}

Related

Why is my array size always equal to zero after I have appended several objects to it?

I've made two functions which call two different rest APIs with an HTTP GET request. I've made a class call "ExchangeObject" which encapsulates the data retrieved from the APIs into an object. In each call, I append an object of class ExchangeObject to an array called exchangesArray.
One property of the ExchangeObject class is called "price" and it's an int value. In viewDidLoad, I call another function that calculates the average of the prices of the two objects in the array. However, the array is always empty! I don't know why. I think it's because I'm making asynchronous calls but I don't know how to fix it. However, my tableview isn't empty. It displays the two prices of the objects in the array.
class ViewController: UIViewController, UITableViewDataSource{
#IBOutlet weak var exchangesTableView: UITableView!
var exchangesArray = [ExchangeObject]()
override func viewDidLoad() {
super.viewDidLoad()
exchangesTableView.dataSource = self
gemini()
bitfinex()
average()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return exchangesArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = exchangesTableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = exchangesArray[indexPath.row].name
cell?.detailTextLabel?.text = exchangesArray[indexPath.row].price
return cell!
}
func gemini(){
let url = "https://api.gemini.com/v1/pubticker/ethusd"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { (data, response, error) in
if(error != nil){
print("Error")
}
else{
do{
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
let price = fetchedData["last"] as! String
self.exchangesArray.append(ExchangeObject(name: "Gemini", price: price))
DispatchQueue.main.async {
self.exchangesTableView.reloadData()
}
}
catch{
print("Error")
}
}
}
task.resume()
}
func bitfinex(){
let url = "https://api.bitfinex.com/v2/ticker/tETHUSD"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { (data, response, error) in
if(error != nil){
print("Error")
}
else{
do{
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSArray
let priceInt = fetchedData[6]
let price = String(describing: priceInt)
self.exchangesArray.append(ExchangeObject(name: "Bitfinex", price: price))
DispatchQueue.main.async {
self.exchangesTableView.reloadData()
}
}
catch{
print("Error")
}
}
}
task.resume()
}
func average() -> Void{
var total: Int = 0
var average: Int
for eth in self.exchangesArray{
total = total + Int(eth.price)!
}
average = total/self.exchangesArray.count
print("The average is: \(average)")
}
}
class ExchangeObject{
var name: String
var price: String
init(name: String, price: String){
self.name = name
self.price = price
}
}
You misunderstand how async methods work. Your methods gemini() and bitfinex() are both asynchronous. They both start data tasks on a background thread, and return immediately, before the data tasks have even begun to execute. At some later time the data task completes and the completion method of your data task gets called. By then, though, your average() method has already been called.
You should rewrite both of those methods to take completion handlers:
func bitfinex(completion: () -> void) {
let url = "https://api.bitfinex.com/v2/ticker/tETHUSD"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { (data, response, error) in
if(error != nil) {
print("Error")
}
else {
do {
let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSArray
let priceInt = fetchedData[6]
let price = String(describing: priceInt)
self.exchangesArray.append(ExchangeObject(name: "Bitfinex", price: price))
DispatchQueue.main.async {
completion()
}
}
catch {
print("Error")
}
}
}
task.resume()
}
Refactor the gemini() method the same way, and then call them like this:
gemini() {
self.bitfinex() {
self.tableView.reloadData()
self.average()
}
}
What you've done is to set up the gemini() and bitfinex() methods so that they each take a block of code as a parameter. They do their thing, and then run that code once the async work is done.
The completion block for the gemini() function calls bitfinex(), and the completion block for bitfinex() reloads the table view and then calls your average method.
I left out some details like what to do if the data task returns an error, but that should be enough to get you started.
Also note that it looks like your two methods are pretty much identical other than the URLs they point to and the names they use to create their output ExchangeObjects. It would make more sense to create a general purpose function:
func getStock(symbol: String, remoteURL: String, completion: () -> void)
And then call that twice with different parameters:
getStock(symbol: "gemini",
remoteURL: "https://api.gemini.com/v1/pubticker/ethusd") {
self.getStock(symbol: "Bitfinex", remoteURL: "https://api.bitfinex.com/v2/ticker/tETHUSD") {
self.tableView.reloadData()
self.average()
}
}

Storing data from an asynchronous closure during URLSession.shared.dataTask

I have tried two techniques to get data and fill an array from completion handlers. In both methods, the dataArray count is showing as 0. Whereas I'm able to put breakpoints and see that the array is being populated when the execution is within the closure:
First Method Tried:
In the below code, dataArray shows a count of zero even though it is populating the dataArray during execution of both inner and outer completionHandlers.
class ViewController: UIViewController {
var dataArray = []
var urlOuter = URL(string: "outer.url.com/json")
override func viewDidLoad() {
super.viewDidLoad()
self.downloadTask()
print(dataArray.count)
}
func downloadTask() {
let outerTask = URLSession.shared.dataTask(with: urlOuter!, completionHandler: {
(data, response, error) in
let parsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! [[String: Any]]
for arr in parsedData! {
var urlInner = URL(string: "http://inner.url/" + arr["url"] + ".com/json")
let innerTask = URLSession.shared.dataTask(with: urlInner!, completionHandler: {
(data, response, error) in
let innerParsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! [[String: Any]]
self.dataArray.append(innerParsedData)
})
innerTask.resume()
}// end of for loop
})
outerTask.resume()
}
}
Second Method Tried:
protocol MyDelegate{ func didFetchData(data:String)}
class ViewController: UIViewController {
var dataArray = []
var urlOuter = URL(string: "outer.url.com/json")
override func viewDidLoad() {
super.viewDidLoad()
self.downloadTask()
print(dataArray.count)
}
func didFetchData(data:String) {
self.dataArray.append(data)
}
func downloadTask() {
let outerTask = URLSession.shared.dataTask(with: urlOuter!, completionHandler: {
(data, response, error) in
let parsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! [[String: Any]]
for arr in parsedData! {
var urlInner = URL(string: "http://inner.url/" + arr["url"] + ".com/json")
let innerTask = URLSession.shared.dataTask(with: urlInner!, completionHandler: {
(data, response, error) in
let innerParsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! String
self. didFetchData(data:innerParsedData)
})
innerTask.resume()
}// end of for loop
})
outerTask.resume()
}}}
Please help me understand how to get data out of the closures and store them in the array. Other solutions suggested are to use delegates and that is what I tried in method 2. Thank you.
You are querying the array in the viewDidLoad method right after you call to populate it in a async method.
check the results in the didFetchData() second method.
override func viewDidLoad() {
super.viewDidLoad()
self.downloadTask()
}
func didFetchData(data:String) {
self.dataArray.append(data)
// Check the count here!!
print(dataArray.count)
}
You will need to change your protocol to:
protocol MyDelegate{ func didFetchData(dataArray: [])}
Then add the variable for the delegate:
var mDelegate = MyDelegate?
Then assign your result:
func didFetchDataCompeleted(dataArray: []) {
// hand over the data to the delegate
mDelegate?.didFetchData(self.dataArray)
}
now change the the call when the innerTask is completed within your closure code to
self.didFetchDataCompeleted(dataArray:self.dataArray)
or just call:
self.mDelegate?.didFetchData(self.dataArray)
when the innerTask is finished
I haven't look to closely but you seemed to be appending to the array correctly. Where you went wrong is asking for the count too soon. URL requests are run asynchronously and takes ages from the CPU's perspective:
self.downloadTask() // this function run async
print(dataArray.count) // nothing has been downloaded yet
try this:
func downloadTask(completionHandler: () -> Void) {
let outerTask = URLSession.shared.dataTask(with: urlOuter!) { data, response, error in
let parsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! [[String: Any]]
let group = DispatchGroup()
for arr in parsedData! {
var urlInner = URL(string: "http://inner.url/" + arr["url"] + ".com/json")
group.enter()
let innerTask = URLSession.shared.dataTask(with: urlInner!) { data, response, error in
let innerParsedData = try JSONSerialization.jsonObject(with: content, options: .mutableContainers) as! [[String: Any]]
// Appending to an array concurrently from multiple queues can lead to
// weird error. The main queue is serial, which make sure that the
// array is appended to once at a time
DispatchQueue.main.async {
self.dataArray.append(innerParsedData)
}
group.leave()
}
innerTask.resume()
}// end of for loop
// Make sure all your inner tasks have completed
group.wait(timeout: .forever)
completionHandler()
}
outerTask.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
self.downloadTask() {
print(dataArray.count)
}
}

Swift - get results from completion handler

I have this method that is inside a class called WebService, inside this method I am getting data from an API:
func GetTableDataOfPhase(phase: String, completion: (result: AnyObject) -> Void)
{
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let requestString = NSString(format:"%#?jobNo=%#", webservice, phase) as String
let url: NSURL! = NSURL(string: requestString)
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
dispatch_async(dispatch_get_main_queue(),
{
do
{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as? [AnyObject]
completion(result: json!)
}
catch
{
print(error)
}
})
})
task.resume()
}
Now I am calling this method from another class like so:
WebService().GetTableDataOfPhase("ORC0005")
{
(result: AnyObject) in
self.data = result as! NSArray
}
This works as expected. Now I am trying to get the results from the completion handler
so I can do this:
WebService().GetTableDataOfPhase("ORC0005")
{
(result: AnyObject) in
self.data = result as! NSArray
}
print(self.data.count)
right now self.data.count is 0, but when I put this print statement inside the curly braces, it is 70, how do I get the results outside the curly braces so I can use self.data.count ?
OK, here is your problem, you're calling dataTaskWithURL(async).
At the time you do:
print(self.data.count)
Your web service call is not finished yet.
When you put this line inside the curly braces, it only runs when the call has a response. That's why it works as expected.
It's a matter of timing, you're tying to evaluate a value that's not there yet.
In your class add
var yourData:NSArray?
And in your method
WebService().GetTableDataOfPhase("ORC0005")
{
(result: AnyObject) in
for res in result
{
self.yourData.append(res)
}
}
dispatch_async(dispatch_get_main_queue(), {
print(self.yourData.count)
}

How to use JSON Results created in a function [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 6 years ago.
I'm parsing JSON data from a remote service. i wrote a function wich do the parsing process. This function has a return value. The result is created in this function and saved in a global property. But when i call the function in viewDidLoad i get an empty result:
Here is my code
class ViewController: UIViewController {
var rates = [String:AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
print(getRates("USD")) // <- Gives me an empty Dictionary
}
func getRates(base: String) -> [String:AnyObject]{
let url = NSURL(string: "http://api.fixer.io/latest?base=\(base)")!
let urlSession = NSURLSession.sharedSession()
let task = urlSession.dataTaskWithURL(url) { (data, response, error) in
do{
self.rates = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as! [String:AnyObject]
//print(self.rates) //<-- Gives me the right output, but i want to use it outside.
}
catch{
print("Something went wrong")
}
}
task.resume()
return self.rates //<- returns an empty Dictionary
}
I can only get the right result inside the function, but I can't use it outside. What is wrong here?
EDIT:
Tank you! All answers are working, but is there a way to store the result in a global property so that i can use the result anywhere? Assuming i have a tableView. Then i need to have the result in a global property
You cannot return response value at once - you have to wait until response arrives from network. So you have to add a callback function (a block or lambda) to execute once response arrived.
class ViewController: UIViewController {
var rates = [String:AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
getRates("USD"){(result) in
print(result)
}
}
func getRates(base: String, callback:(result:[String:AnyObject])->()){
let url = NSURL(string: "http://api.fixer.io/latest?base=\(base)")!
let urlSession = NSURLSession.sharedSession()
let task = urlSession.dataTaskWithURL(url) { (data, response, error) in
do{
self.rates = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as! [String:AnyObject]
callback(self.rates)
//print(self.rates) //<-- Gives me the right output, but i want to use it outside.
}
catch{
print("Something went wrong")
}
}
task.resume()
}
Because you are using NSURLSession and the task is asynchronous you will need to use a completion handler. Here is an example:
//.. UIViewController Code
var rates = [String: AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
getRates("USD") { [weak self] result in
self?.rates = result
}
}
func getRates(base: String, completion: [String: AnyObject] -> Void) {
let url = NSURL(string: "http://api.fixer.io/latest?base=\(base)")!
let urlSession = NSURLSession.sharedSession()
let task = urlSession.dataTaskWithURL(url) { (data, response, error) in
do {
let rates = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as! [String:AnyObject]
completion(rates)
}
catch {
print("Something went wrong")
}
}
task.resume()
}
Try this on your code:
class ViewController: UIViewController {
var rates = [String:AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
getRates() { (result) in
print(result)
}
}
func getRates(completion: (result: Array)) -> Void{
let url = NSURL(string: "http://api.fixer.io/latest?base=\(base)")!
let urlSession = NSURLSession.sharedSession()
let task = urlSession.dataTaskWithURL(url) { (data, response, error) in
do{
self.rates = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as! [String:AnyObject]
completion(self.rates)
}
catch{
print("Something went wrong")
}
}
task.resume()
return self.rates //<- returns an empty Dictionary
}
}

Include a return handler in async call in Swift

I'm experimenting with async calls but I'm a little lost. The print(json) in the viewDidLoad outputs an empty dictionary, but the one within the function prints correctly. This is unsurprising; it gets to that print before the async is completed. I can't figure out how to fix it; I tried putting the return within the completion handler, but I got an error that Unexpected non-void return value in void function. I tried changing the completion handler to expect a return value, but either that's not the right approach or I was doing it wrong.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let json = getJson("https://maps.googleapis.com/maps/api/geocode/json?address=WashingtonDC&sensor=false")
print(json)
}
func getJson(url: String) -> AnyObject {
var json:AnyObject = [:]
let urlPath = NSURL(string: url)
let urlRequest = NSURLRequest(URL: urlPath!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
if error != nil {
print("Error")
} else {
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
print(json)
} catch {
print("json error")
}
}
})
task.resume()
return json
}
}
You will need to have a completion handler based interface to your async API.
func getJson(url: String, completion : (success: Bool, json: AnyObject? ) ->Void ) -> Void {
var json:AnyObject = [:]
let urlPath = NSURL(string: url)
let urlRequest = NSURLRequest(URL: urlPath!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
if error != nil {
print("Error")
} else {
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
print(json)
//Call the completion handler here:
completion(success : true, json :json )
} catch {
print("json error")
completion(success : false, json :nil )
}
}
})
task.resume()
}
}
Now you call call this API as follows-
override func viewDidLoad() {
super.viewDidLoad()
getJson("https://maps.googleapis.com/maps/api/geocode/json?address=WashingtonDC&sensor=false") { (success, json) -> Void in
if success {
if let json = json {
print(json)
}
}
}
}

Resources