Swift - get results from completion handler - ios

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)
}

Related

Alamofire ignoring closure that sets/handles data

I am using Alamofire to perform a network request to the dummy data source https://jsonplaceholder.typicode.com/posts and render it in my application.
I have a file called NetworkingClient.swift that abstracts most of this logic out and allows is to be reused.
public class NetworkingClient {
typealias WebServiceResponse = ([[String: Any]]?, Error?) -> Void
func execute(_ url: URL, completion: #escaping WebServiceResponse) {
Alamofire.request(url).validate().responseJSON { response in
print(response)
if let error = response.error {
completion(nil, error)
} else if let jsonArray = response.result.value as? [[String: Any]] {
completion(jsonArray, nil)
} else if let jsonDict = response.result.value as? [String: Any] {
completion([jsonDict], nil)
}
}
}
}
I call the execute in a set up function I have in my main view controller file:
func setUpView() {
let networkingClient = NetworkingClient()
let posts_endpoint = "https://jsonplaceholder.typicode.com/posts"
let posts_endpoint_url = URL(string: TEST_URL_STRING)
networkingClient.execute(posts_endpoint_url) { (json, error) in
if let error = error {
print([["error": error]])
} else if let json = json {
print(json)
}
}
}
Where I call this inside viewDidLoad() under super.viewDidLoad()
I've set breakpoints inside the response in closure and I wasn't able to trigger any of them, in fact I think it's skipping the entire thing completely and I don't know why.
I am following this youtube video where the video guide does the exact same thing except their request goes through.
What am I missing?
I am using Swift 4, XCode 10, running on iOS 12.1 and my AlamoFire version is 4.7.
It's all about async stuff.your are declaring NetworkingClient object in func called setupView and Alamofire using .background thread to do stuff.so time executing of networkingClient.execute is not clear and after that setUpView deallocate from memory and all it's objects are gone including NetworkingClient.so for preventing this just declare let networkingClient = NetworkingClient() outside of function

Sending string from JSON data to variable outside of the function

I am attempting to take a string from JSON data and set it to a variable. My problem is that the variable shows as empty. I am using JSONDecoder to retrieve the JSON data and setting the string to a variable outside of the function. I then want to use that variable inside of another function
When I print the variable it still shows up as blank even after the function has loaded. Within the function the string appears correctly.
Code:
var filmTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
loadFilms()
print(self.filmTitle) //Prints as an empty string
}
func loadFilms() {
let id = filmId
let apiKey = "97a0d64910120cbeae9df9cb675ad235"
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)?api_key=\(apiKey)&language=en-US")
let request = URLRequest(
url: url! as URL,
cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData,
timeoutInterval: 10 )
let session = URLSession (
configuration: URLSessionConfiguration.default,
delegate: nil,
delegateQueue: OperationQueue.main
)
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let details = try! JSONDecoder().decode(Details.self, from: data)
self.filmTitle = details.title
print(self.filmTitle) //string prints correctly
}
}
})
task.resume()
}
What am I missing to correctly set the string to the variable?
Loading data from the internet is an asynchronous method. The print statement is being called before loadFilms() has completed.
Use a callback to get the data after it has completed.
func loadFilms(completion: #escaping (Details?, Error?) -> Void) {
//...
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let details = try JSONDecoder().decode(Details.self, from: data)
completion(details, nil)
} catch {
completion(nil, error)
}
})
}
At the call site:
override func viewDidLoad() {
loadFilms { details, error in
if error { //* Handle Error */ }
self.filmTitle = details.title
print(filmTitle)
}
}
Web request are asynchronous and from the CP's perspective, take a long time to complete. When you call this:
override func viewDidLoad() {
super.viewDidLoad()
loadFilms()
print(self.filmTitle) // loadFilms() hasn't finished so `filmTitle` is empty
}
It's better to set a property observer on filmTitle:
var filmTitle: String? = nil {
didSet {
print(filmTitle)
Dispatch.main.async {
// update your GUI
}
}
}
The solution to this problem was to reload the collection view that the array was being sent to within the decoder function after the data was set to the array.

Swift get value from api possible threading issue

I have a function that does an api request using google's places api. From the api response data I capture a value and try to set it to a variable. This function is called inside another function. I then try to access that variable but unfortunately the variable doesn't contain the value yet. This appears to be a threading issue but I don't know how to fix it.
update:
I have updated the code based on the responses. Unfortunately I am still not able to access the variable with the value from the api request. I have rewrote the function that does the api request to use a completion handler. The mapView(mapView: GMSMapView!, didTapInfoWindowOfMarker marker: GMSMarker!) is a function from the google maps framework. Would I need to rewrite this as well to use take a completion handler ?
// variable
var website = ""
// code with api request
func getWebsite2(id: String, completion: (result: String) -> Void) {
var url = NSURL(string: "https://maps.googleapis.com/maps/api/place/details/json?placeid=\(id)&key=AIzaSyAWV1BUFv_vcedYroVrY7DWYuIxcHaqrv0")
self.dataTask = defaultSession.dataTaskWithURL(url!) {
data, respnse, error in
let json : AnyObject
do {
json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
var dictionArr = json["result"]
self.website = dictionArr!!["website"] as! String
print(self.website)
}
catch {
print(error)
}
}
self.dataTask?.resume()
}
// second function
func mapView(mapView: GMSMapView!, didTapInfoWindowOfMarker marker: GMSMarker!) {
let storeMarker = marker as! PlaceMarker
self.getWebsite2(storeMarker.id!) {
(result: String) in
print("inside did tap")
print(self.website)
// problem still here
// above two lines of code never run
}
self.performSegueWithIdentifier("toWebView", sender: nil)
}
// I initialize defaultSession and dataTask like this.
let defaultSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
var dataTask: NSURLSessionDataTask?
You are not invoking the completion handler passed into the getWebsite2 function. This (pseudo)code shows how to take the string received from the server and pass it to the closure invoked in didTapInfoWindowOfMarker.
func getWebsite2(id: String, completion: (result: String) -> Void) {
self.dataTask = defaultSession.dataTaskWithURL(url!) {
data, response, error in
// now on background thread
let someStringFromNetwork = data[0]
dispatch_async(dispatch_get_main_queue(),{
completion(someStringFromNetwork)
})
}
}
Firstly do not force unwrapping of the variables and always use do{} catch{} where it is required.
This small code block that show how you should handle try and if let conditions:
do {
let jsonObject = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String:AnyObject]
if let dictionary = jsonObject["result"] as? [String: String] {
self.website = dictionary["website"]
} else {
print("Parse error")
}
} catch {
print("JSON error: \(error)")
}
Secondly defaultSession.dataTaskWithURL is asynchronous request that will set data only when he will finish.
In another worlds you try to print value when request is not finished.
For solving of youre problem you should use Completion Handlers.

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
}
}

Global variable doesn't store any data in anonymous method

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()
}

Resources