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
}
}
Related
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)
}
}
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()
}
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)
}
}
}
}
I am trying to display some data I take from a data base into a TableView but the data is not shown in the TableView. The data I receive is formatted in JSON.
This is the data I receive form the data base and what I want to print in the TableViewis just David:
{"name":"David"}
This is the code to get the data from de data base:
let session = NSURLSession.sharedSession()
let request = NSURLRequest(URL: NSURL(string: "http://localhost:8888/Patients.php")!)
let task = session.dataTaskWithRequest(request) {data, response, downloadError in
if let error = downloadError {
print("Could not complete the request \(error)")
}
else {
do {
self.json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
} catch {
fatalError()
}
dispatch_async(dispatch_get_main_queue(), {
if let parseJSON = self.json{
let name = parseJSON["name"] as! String
self.arrayData.append(name)
print("Data1:\(self.arrayData)")
}
})
}
}
task.resume()
arrayData is an array where I put the data I receive from the data base and it is declared like this:
var arrayData: [String] = []
The code
print("Data1:\(self.arrayData)")
show in the console this Data1:["David"], so I get the data correctly.
Then I implement the methods to print in the ´TableView´, the numberOfSectionsInTableViewmethod, the numberOfRowsInSection method and the cellForRowAtIndexPath method but the data is not printed in the TableView.
I think the problem is that the TableViewis drawn before I put the data in the array so it prints nothing because the array is empty, and I don´t know how to fix it.
Anyone knows what is my problem?
yes, you're right.
session.dataTaskWithRequest
is async. Data is not returned immediately, it have delay.
You must to reload tableview after recieved data:
self.arrayData.append(name)
self.tableview.reloadData()
Usually i will use block:
func getData(handleComplete:((arr:NSMutableArray)->())){
let aray = NSMutableArray()
let session = NSURLSession.sharedSession()
let request = NSURLRequest(URL: NSURL(string: "http://localhost:8888/Patients.php")!)
let task = session.dataTaskWithRequest(request) {data, response, downloadError in
if let error = downloadError {
print("Could not complete the request \(error)")
}
else {
do {
self.json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
} catch {
fatalError()
}
dispatch_async(dispatch_get_main_queue(), {
if let parseJSON = self.json{
let name = parseJSON["name"] as! String
aray.append(name)
print("Data1:\(self.arrayData)")
}
handleComplete(aray)
})
}
}
task.resume()
arrayData
}
and in viewdidload i will call:
override func viewDidLoad() {
super.viewDidLoad()
self.getData { (arr) -> () in
self.tableview.reloadData()
}
}
it 's better