This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 4 years ago.
I am new to Swift and was wondering how can I get a value from an Async task. I have a function that gets Json data from an API on return I would like to get the value of a particular field outside of the async task... My code is below essentially I have a variable called status I want to get the value of status after the async called is returned then I want to check if the value is 1 . In the code below the value returned is 1 however it seems like the async called is executed before the line if Status == 1 {} . If the value is One then I want to navigate to a different ViewController . Any suggestions would be great ... I obviously can not put the code to go to a different ViewController inside the Async code since that is called many times .
func GetData() {
var status = 0
// Code that simply contains URL and parameters
URLSession.shared.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
print("Error")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
DispatchQueue.main.async {
if let Replies = parsedData["Result"] as? [AnyObject] {
for Stream in Replies {
if let myvalue = Stream["status"] as? Int {
status = myvalue
}
}
}
}
} catch let error as NSError {
print(error)
}
}
}).resume()
if status == 1 {
// This code is executed before the async so I don't get the value
let nextViewController = self.storyboard?.instantiateViewController(withIdentifier: "Passed") as! Passed
self.present(nextViewController, animated:false, completion:nil)
}
}
You can use a callback function for that like this:
func GetData(callback: (Int) -> Void) {
//Inside async task, Once you get the values you want to send in callback
callback(status)
}
You will get a callback from where you called the function.
For your situation, Anbu's answer will work too.
Related
I have an issue with using DispatchGroup (as it was recommended here) with FireStore snapshotListener
In my example I have two functions. The first one is being called by the ViewController and should return array of objects to be displayed in the View.
The second one is a function to get child object from FireStore for each array member. Both of them must be executed asynchronously. The second one should be called in cycle.
So I used DispatchGroup to wait till all executions of the second function are completed to call the UI update. Here is my code (see commented section):
/// Async function returns all tables with active sessions (if any)
class func getTablesWithActiveSessionsAsync(completion: #escaping ([Table], Error?) -> Void) {
let tablesCollection = userData
.collection("Tables")
.order(by: "name", descending: false)
tablesCollection.addSnapshotListener { (snapshot, error) in
var tables = [Table]()
if let error = error {
completion (tables, error)
}
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let firebaseID = document.documentID
let tableName = data["name"] as! String
let tableCapacity = data["capacity"] as! Int16
let table = Table(firebaseID: firebaseID, tableName: tableName, tableCapacity: tableCapacity)
tables.append(table)
}
}
// Get active sessions for each table.
// Run completion only when the last one is processed.
let dispatchGroup = DispatchGroup()
for table in tables {
dispatchGroup.enter()
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(tables, nil)
}
}
}
/// Async function returns table session for table or nil if no active session is opened.
class func getActiveTableSessionAsync (forTable table: Table, completion: #escaping (TableSession?, Error?) -> Void) {
let tableSessionCollection = userData
.collection("Tables")
.document(table.firebaseID!)
.collection("ActiveSessions")
tableSessionCollection.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil, error)
return
}
if let snapshot = snapshot {
guard snapshot.documents.count != 0 else { completion(nil, error); return }
// some other code
}
completion(nil,nil)
}
}
Everything works fine till the moment when the snapshot is changed because of using a snapshotListener in the second function. When data is changed, the following closure is getting called:
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
And it fails on the dispatchGroup.leave() step, because at the moment group is empty.
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All dispatchGroup.enter() and dispatchGroup.leave() are already done on this step. And this closure was called by listener separately.
I tried to find the way how to check if the DispatchGroup is empty to do not call leave() method. But did not find any native solution.
The only similar solution I've found is in the following answer. But it looks too hacky and not sure if will work properly.
Is there any way to check if DispatchGroup is empty? According to this answer, there is no way to do it. But probably something changed during last 2 years.
Is there any other way to fix this issue and keep snapshotListener in place?
For now I implemented some kind of workaround solution - to use a counter.
I do not feel it's the best solution, but at least work for now.
// Get active sessions for each table.
// Run completion only when the last one is processed.
var counter = tables.count
for table in tables {
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
counter = counter - 1
if (counter <= 0) {
completion(tables, nil)
}
})
}
Hello I have been trying alot and looking around on SO, but I cant find a good solution to my problem.
The problem is that i do 2 requests within one function and the first one finishes, then it does updateUI function on the main thread instead of waiting for the second download to finish.
I know that i am doing something wrong but in my mind the two requests work on different threads and that is why updateUI only will trigger after first download is complete.
Is it possible to use dispatchqueue? I dont know how completionhandlers work either sadly..
I couldnt see what i have to use from their github page either, im quite new at Swift.
Please do not set this as duplicate. would really appreciate it
func getMovieData(){
self.posterLoading.startAnimating()
//Set up URL
let testCall: String = "https://api.themoviedb.org/3/discover/movie?api_key=935f5ddeed3fb57e&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres=12"
Alamofire.request(testCall).responseJSON { response in
//print(response.request) // original URL request
//print(response.response) // HTTP URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
if let json = response.result.value as? Dictionary<String,AnyObject> {
if let movies = json["results"] as? [AnyObject]{
for movie in movies{
let movieObject: Movie = Movie()
let title = movie["title"] as! String
let releaseDate = movie["release_date"] as! String
let posterPath = movie["poster_path"] as! String
let overView = movie["overview"] as! String
let movieId = movie["id"] as! Int
let genre_ids = movie["genre_ids"] as! [AnyObject]
movieObject.title = title
movieObject.movieRelease = releaseDate
movieObject.posterPath = posterPath
movieObject.overView = overView
movieObject.movieId = movieId
for genre in genre_ids{//Genre ids, fix this
movieObject.movieGenre.append(genre as! Int)
}
Alamofire.request("http://image.tmdb.org/t/p/w1920" + posterPath).responseImage {
response in
debugPrint(response)
//print(response.request)
//print(response.response)
//debugPrint(response.result)
if var image = response.result.value {
image = UIImage(data: response.data!)!
movieObject.poster = image
}
}
self.movieArray.append(movieObject)
print("movie added")
}//End of for each movie
DispatchQueue.main.async(){
print("is ready for UI")
self.updateUI()
}
}
}
}//End of Json request
}//End of getmoviedata
func updateUI(){
uiMovieTitle.text = movieArray[movieIndex].title
uiMoviePoster.image = movieArray[movieIndex].poster
}
Just make your getMovieData func with a completion block.
func getMovieData(finished: () -> Void) {
Alamofire.request(testCall).responseJSON { response in
// call me on success or failure
finished()
}
}
and then you can call your update UI in the completion block of the func, where you calling it the second time getMovieData()
Your function should look like this.
func getMovieData(completionHandler: #escaping (_ returnedData: Dictionary<String,AnyObject>)-> Void ) {
Alamofire.request(testCall).response { response in
if let JSON = response.result.value {
completionHandler(JSON)
}
}
}
And your function call look like
getMovieData(completionHandler: {(returnedData)-> Void in
//Do whatever you want with your returnedData JSON data.
//when you finish working on data you can update UI
updateUI()
})
You don't have to do your data operations in your function call btw.You can do your stuff in your function and call the completionHandler() end of it. Then you can update your ui only at function call.
By using RxSwift, the purpose of my project is whenever an user types a city in search bar, it will make a call to wrap the current temperature. Currently, I have viewModel which contains
var searchingTerm = Variable<String>("") // this will be binded to search text from view controller
var result: Observable<Weather>! // this Observable will emit the result based on searchingTerm above.
In api service, I'm wrapping a network call using RxSwift by following
func openWeatherMapBy(city: String) -> Observable<Weather> {
let url = NSURL(string: resourceURL.forecast.path.stringByReplacingOccurrencesOfString("EnterYourCity", withString: city))
return Observable<WeatherModel>.create({ observer -> Disposable in
let downloadTask = self.session.dataTaskWithURL(url!, completionHandler: { (data, response, error) in
if let err = error {
observer.onError(err)
}
else {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as! [String: AnyObject]
let weather = Weather(data: json)
observer.onNext(weather)
observer.onCompleted()
}
catch {
}
}
})
downloadTask.resume()
return AnonymousDisposable {
downloadTask.cancel()
}
})
}
As long as the model created, I'll send it to an observer and complete
At view controller, I'm doing
viewModel.result
.subscribe( onNext: { [weak self] model in
self?.weatherModel = model
dispatch_async(dispatch_get_main_queue(), {
self?.cityLabel.text = model.cityName
self?.temperatureLabel.text = model.cityTemp?.description
})
},
onError: { (error) in
print("Error is \(error)")
},
onCompleted:{
print("Complete")
}
)
{ print("Dealloc")}
.addDisposableTo(disposeBag)
}
It works as expected, UI is updated and show me what I want. However, I have just realized that onCompleted never gets called. I assume if I do everything right, I must have it printed out.
Any ideas about this issue. All comments are welcomed here.
result seems to be derived from searchingTerm, which is a Variable.
Variable only complete when they are being deallocated (source) so it makes sense that result does not receive onCompleted.
It makes sense that the behavior is this one. An observable will never emit new values after onCompleted. And you don't want it to stop updating after the first search result is presented.
Let me preface this by saying I'm VERY new to Swift 2 and am building my first app which calls an api (php) for data (JSON). The problem I'm running into is when I make the call to the api the other functions ran before the api can send back the data.
I've researched some type of a onComplete to call a functions after the api response is done. I'm sure for most of you this is easy, but I cant seem to figure it our.
Thanks in advance!
class ViewController: UIViewController {
var Selects = [Selectors]()
var list = [AnyObject]()
var options = [String]()
var index = 0
#IBOutlet var Buttons: [UIButton]!
override func viewDidLoad() {
super.viewDidLoad()
self.API()
self.Render()
}
func API() {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
print("request failed \(error)")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
}
task.resume()
}
func BuildOptions() {
// BuildOptions stuff happens here
}
func Render() {
// I do stuff here with the data
}
}
So I assume your Render() method is called before data gets back from the api? Keeping your api-calling code in the view controllers is bad design, but as you're new i won't expand on that. In your case it's as simple as not calling your Render() method in viewDidLoad() - call it after you're done with parsing the data from JSON (after the self.Selects = [Selectors... line). NSURLSession.sharedSession().dataTaskWithRequest(request) method is called asynchronously , and the callback block with data, response, error parameters is executed after this method is done with fetching your data, so it can happen after the viewDidLoad is long done and intially had no data to work on as the asynchronous method was still waiting for response from the API.
Edit - speaking of handling api calls, it's a wise thing to keep them separated from specific view controllers to maintain a clean reusable code base. You should call the API and wait for a callback from it, so i'd just do that to your API function, it would look like this:
static func callAPI(callback: [AnyObject]? -> Void ) {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
completion(nil)
}
do {
var list = [AnyObject]()
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
completion(list)
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
completion(nil)
}
}
task.resume()
}
Generally speaking methods should do one specific thing - in your case call the api and return data or error. Initialize your selectors in the view controllers on callback. Your view controller's viewDidLoad would look like this using the code above:
override func viewDidLoad() {
super.viewDidLoad()
YourApiCallingClass.callApi() {
result in
if let list = result {
self.list = list
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
self.Render()
} else {
//Handle situation where no data will be returned, you can add second parameter to the closue in callApi method that will hold your custom errors just as the dataTaskWithRequest does :D
}
}
}
Now you have a nice separation of concerns, API method is reusable and view controller just handles what happens when it gets the data. It'd be nice if you slapped an UIActivityIndicator in the middle of the screen while waiting, it'd look all neat and professional then :P
I'm having trouble retrieving data from my Alamofire request asynchronously.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String) -> Book {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
Alamofire.request(.GET, url).response { response in
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
}
let book = Book(title: title)
print(book.title)
return book
}
}
The output of print(book.title) is "", and I understand this is because the print statement is running before the request returns.
How do I get the book instance to be returned only when it is instantiated with the data from the request?
The problem you have is that you are calling an asynchronous method and expecting to return the result synchronously. When your code is executed, the getBook function completes and returns before even the GET request has complete.
Basically, you have two options:
Update your getBook method to be asynchronous and return the result with a completion block/callback
Wait for the asynchronous call to complete, blocking the current thread (this is OK as long as it is not the main thread you are blocking), and return the result synchronously.
1. Update your method to be asynchronous
To do this, you must return the result on a block/callback function.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String, complete: (book: Book?, error: NSError?) -> Void) {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
Alamofire.request(.GET, url).response { request, response, data, error in
// TODO: You should check for network errors here
// and notify the calling function and end-user properly.
if error != nil {
complete(book: nil, error: error as? NSError)
return
}
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
let book = Book(title: title)
print(book.title)
complete(book: book, error: nil)
}
}
}
As mentioned in the above code, ideally you should handle errors in the callback response (including exceptions while parsing the JSON). Once handled, you can update the callback parameters to (book: Book?, error: NSError?) -> Void or similar, and check for book or error to be set on the caller function.
To call the function, you need to pass a block to handle the response:
BookGetter.instance.getBook("bookID") { (book, error) in
if error != nil {
// Show UIAlertView with error message (localizedDescription)
return
}
// Update User Interface with the book details
}
2. Wait for the asynchronous call to complete
As mentioned above, this is a good idea only if you were running this code on a background thread. It is OK to block background threads, but it is never OK to block the main thread on a graphic application, as it will freeze the user interface. If you do not know what blocking means, please use the option #1.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String) -> Book {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
let semaphore = dispatch_semaphore_create(0)
Alamofire.request(.GET, url).response { response in
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
dispatch_semaphore_signal(semaphore)
}
//Wait for the request to complete
while dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW) != 0 {
NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 10))
}
let book = Book(title: title)
print(book.title)
return book
}
}
You can use closures and return a completionHandler with your book like in the following way:
func getBook(bookId: String, completionHandler: (book: Book?) -> ()) {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
Alamofire.request(.GET, url).response { response in completionHandler(
book:
{
// In this block you create the Book object or returns nil in case of error
if response == nil {
return nil
}
let jsonDict = JSON(data: response.2!)
let title = String(jsonDict["items"][0]["volumeInfo"]["title"])
let book = Book(title: title)
return book
}())
}
}
And then you can call it like in the following way:
getBook("idOfYourBook") { book in
if let book = book {
println(book.title)
}
}
I hope this help you.