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)
}
}
Related
I have a table in a view controller that is populated through a dictionary from which information is retrieved via a JSON request. In the viewDidLoad() function, I call the function that retrieves the data which is added to `IncompletedDeadlines dictionary:
override func viewDidLoad() {
super.viewDidLoad()
self.IncompleteDeadlines = [String:AnyObject]()
self.retrieveIncompletedDeadlines()
}
Everything works however the table only shows when interacted with. I thought maybe the best way to show the table the moment the view appears is by adding a tableView.reload to viewDidAppear as so:
override func viewDidAppear(_ animated: Bool) {
self.tableView.reloadData()
}
But this doesn't fix it. I have attached pictures for clarity of the situation. Picture one shows the view the moment the view appears. Picture 2 only happens once the table is interacted with i.e. swiped. So my question is how can I get the table to show immediately? I understand there can be a delay because of the load, but I shouldn't have to interact with it for it to show:
When the view is interacted with i.e. swiped:
The retrieveIncompletedDeadlines() function is as so:
func retrieveIncompletedDeadlines(){
let myUrl = NSURL(string: "https://www.example.com/scripts/retrieveIncompleteDeadlines.php");
let request = NSMutableURLRequest(url:myUrl! as URL)
let user_id = UserDetails[0]
request.httpMethod = "POST";
let postString = "user_id=\(user_id)";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(String(describing: error))")
return
}
var err: NSError?
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let checker:String = parseJSON["status"] as! String;
if(checker == "Success"){
let resultValue = parseJSON["deadlines"] as! [String:AnyObject]
self.IncompleteDeadlines = resultValue
}
self.tableView.reloadData()
}
} catch let error as NSError {
err = error
print(err!);
}
}
task.resume();
self.tableView.reloadData()
}
JSON will be parsed on the background thread but any update to the UI must be done on the main thread hence you have to do it inside DispatchQueue.main.async {} This article explains well what is the problem.
Furthermore I would write a completions handler which returns the data once the operation has finished. This is another interesting article about.
Completion handlers are super convenient when your app is doing something that might take a little while, like making an API call, and you need to do something when that task is done, like updating the UI to show the data.
var incompleteDeadlines = [String:AnyObject]()
override func viewDidLoad() {
super.viewDidLoad()
//please note your original function has changed
self.retrieveIncompletedDeadlines { (result, success) in
if success {
// once all the data has been parsed you assigned the result to self.incompleteDeadlines
self.incompleteDeadlines = result
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
func retrieveIncompletedDeadlines(_ completion:#escaping ([String:AnyObject] , _ success: Bool)-> Void){
let myUrl = NSURL(string: "https://www.example.com/scripts/retrieveIncompleteDeadlines.php");
let request = NSMutableURLRequest(url:myUrl! as URL)
let user_id = UserDetails[0]
request.httpMethod = "POST";
let postString = "user_id=\(user_id)";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(String(describing: error))")
return
}
var err: NSError?
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let checker:String = parseJSON["status"] as! String;
var resultValue = [String:AnyObject]()
if(checker == "Success"){
resultValue = parseJSON["deadlines"] as! [String:AnyObject]
}
completion(resultValue, true)
}
} catch let error as NSError {
err = error
print(err!);
}
}
task.resume();
}
}
My error should be quite obvious but I can't find it;
I've a global variable initialized a the beginning of my class:
class InscriptionStageViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
var lesSemaines = [String]()
I try to populate this array with a distant json file using that function
func getSemainesStages(){
let url = URL(string: "http://www.boisdelacambre.be/ios/json/semaines.json")
let task = URLSession.shared.dataTask(with: url!){ (data, response, error) in
if let content = data {
do {
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
let listeSemaines = myJson["semaine"] as! [[String:AnyObject]]
//print(listeSemaines)
for i in 0...listeSemaines.count-1 {
var tabSem = listeSemaines[i]
let intituleSemaine:String = tabSem["intitule"] as! String
//let dateSemaine:String = tabSem["date"] as! String
DispatchQueue.main.sync
{
self.lesSemaines.append(intituleSemaine)
}
}
} catch
{
print("erreur Json")
}
}
}
task.resume()
}
When I call my function in the viewDidLoad and then I print my global array, it's empty (the URL is correct, the json data is read correctly and when I read the data appended in the array in the loop, it print the (so) needed value...)
Thanks in advance
The download takes time. Introduce another methode:
func updateUi() {
print(lesSemaines)
//pickerView.reloadAllComponents()
}
And call it after the download finished:
func getSemainesStages(){
// ... your code
let task = URLSession.shared.dataTask(with: url!){ (data, response, error) in
// ... your code
for tabSem in listeSemaines{
guard let intituleSemaine = tabSem["intitule"] as? String else {
print("erreur Json")
continue
}
self.lesSemaines.append(intituleSemaine)
}
// update UI *after* for loop
DispatchQueue.main.async {
updateUi()
}
// ... your code
}
}
I have updated your code to Swift 3. Please replace it with below code.
func getSemainesStages(){
let url = URL(string: "http://www.boisdelacambre.be/ios/json/semaines.json")
let task = URLSession.shared.dataTask(with: url!){ (data, response, error) in
if let content = data {
do {
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String: Any]
let listeSemaines = myJson["semaine"] as! [[String: Any]]
for i in 0...listeSemaines.count-1 {
var tabSem = listeSemaines[i]
let intituleSemaine:String = tabSem["intitule"] as! String
self.lesSemaines.append(intituleSemaine)
}
print(self.lesSemaines)
} catch {
print("erreur Json")
}
}
}
task.resume()
}
In the following code I am looking to pull some attributes from a database - id's who are members of a particular community.
I make another API call to fetch the names of those community members.
import UIKit
class ShowCommunityViewController: UIViewController {
#IBOutlet weak var communityName: UILabel!
var communityIsCalled: String?
var comIds = [String]()
var communityId: Int?
var communityPlayers = [String]()
var communityPlayerIds = [String]()
override func viewDidAppear(_ animated: Bool) {
let myUrl = URL(string: "http://www.quasisquest.uk/KeepScore/specificCommunity.php?");
var request = URLRequest(url:myUrl!);
request.httpMethod = "POST";
let postString = "id=\(comIds[communityId!])";
// print (postString)
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
DispatchQueue.main.async
{
if error != nil {
print("error=\(error)")
return
}
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:AnyObject]
// print (json)
if let arr = json?["players"] as? [[String:String]] {
let players = arr.flatMap { $0["player_id"]!
// print(arr)
}
print ("one ",players)
self.communityPlayerIds = players
}
} catch{
print(error)
}
}
}
task.resume()
let myUrlTwo = URL(string: "http://www.quasisquest.uk/KeepScore/getPlayers.php?");
var requestTwo = URLRequest(url:myUrlTwo!);
requestTwo.httpMethod = "POST";
let postStringTwo = "player_ids=\(self.communityPlayerIds)";
print ("two ",postStringTwo)
requestTwo.httpBody = postStringTwo.data(using: String.Encoding.utf8);
let taskTwo = URLSession.shared.dataTask(with: requestTwo) { (data: Data?, response: URLResponse?, error: Error?) in
DispatchQueue.main.async
{
if error != nil {
print("error=\(error)")
return
}
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String:AnyObject]
// print (json)
if let arr = json?["player_names"] as? [[String:String]] {
let playerNames = arr.flatMap { $0["user_name"]!
// print(arr)
}
print ("three ", playerNames)
}
} catch{
print(error)
}
}
}
taskTwo.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
communityName.text = communityIsCalled
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
If you note the order of the print debug commands one, two, three.
They actually execute in order of two, one, three.
Because two is executing before one my Post String does not have the player_ids required to look up the names.
Could someone please explain the process flow to me?
First, let's strip away everything, leaving just the 3 print statements in your code
let task = URLSession.shared.dataTask(with: request) {
print("one")
}
task.resume()
print("two")
let taskTwo = URLSession.shared.dataTask(with: requestTwo) {
print("three")
}
taskTwo.resume()
URLSession tasks are executed asynchronously. When you call task.resume(), it sends the instructions to another thread and immediately jumps to the next line, without waiting for the task to complete. Network requests are extremely slow compared to the CPU's speed so it will almost always print two before one.
The order of one and three are uncertain, depending on which one is faster for the server to respond.
This is because dataTask(request:) is asynchronous. Both tasks are started nearly at once, but the completionHandler is called, when the HTTP-Request was finished. This can take different amounts of time for each request.
When you use DispatchQueue.main.async you're adding the job to a queue that can run multiple jobs at once, each of those jobs runs in a different amount of time so their results would not necessarily be in the order that you have them in your code.
Theoretically because I'm not familiar with that exact bit of Swift: If there was no call to DispatchQueue.main.async then it would execute them in order but it would be blocking, the code would wait for the network request to complete before moving on and the print statements would be in order.
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
}
}
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()
}