Alert displaying late while getting response from URL - ios

Im trying to display an alert when a user presses a button to insert text in database, everything works fine but when i get the response and try to display it in alert, it doesn't work instantly, the alert is displayed when i press somewhere on the screen, after pressing the button. I need it to come as soon as the button is pressed , if i print the same response it works fine, but when i use alert it doesn't.
#IBAction func posting(sender: AnyObject) {
let request = NSMutableURLRequest(URL: NSURL(string: "https://example.com/appapi/test2.php")!)
request.HTTPMethod = "POST"
let postString = "a=\(title.text!)"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error inenter code here
if error != nil {
print("error=\(error)")
return
}
let responseString = "\(NSString(data: data!, encoding: NSUTF8StringEncoding)!)"
self.displayR(responseString)
}
task.resume()
}
func displayR(str:String){
var alert = UIAlertController(title: "Description", message: "\(str)" ,preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { (action) -> Void in }))
self.presentViewController(alert, animated: true, completion: nil)
}

When you execute data task with request, the code executes in a background thread. Any UI changes (in this case displaying alert) must be done on main thread. To do so put your call to display function in following code:
dispatch_async(dispatch_get_main_queue()) {
//call display function here
}

Related

Why calling a dialog with a struct drops this exception?

I'm trying to redirect the output of this API Call decoded in a struct but when I try to use the data to create a dialog it gives me this weird exception. As you can see, the API returns data but only when I create the dialog I see this exception. Can you help me?
Code:
struct rspServerInfo: Codable{
let ok: Bool
let info: String
}
#IBAction func backendDetails(_ sender: Any) {
guard let url = URL(string: "http://\(hostname):\(port)/STOInfo/ServerInfo")else{
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: AnyHashable] = [
"username": username,
"password": password,
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
let task = URLSession.shared.dataTask(with: request) {data, _, error in
guard let data=data, error == nil else{
return
}
do{
let response = try JSONDecoder().decode(rspServerInfo.self, from: data)
print("SUCCESS: \(response)")
let dialogMessage = UIAlertController(title: "Backend details", message: response.info, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
print("Ok button tapped")
})
dialogMessage.addAction(ok)
self.present(dialogMessage, animated: true, completion: nil)
}
catch{
print(error)
let dialogMessage = UIAlertController(title: "Backend details", message: "Error retreiving.", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
print("Ok button tapped")
})
dialogMessage.addAction(ok)
self.present(dialogMessage, animated: true, completion: nil)
}
}
task.resume()
}
The error message give a big clue:
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Modifications to the
layout engine must not be performed from a background thread after it
has been accessed from the main thread.' terminating with uncaught
exception of type NSException
The URLRequest run asynchronously on a background thread, including its completion handlers. You (generally) can't do UI work outside of the main thread. To display the results you need to push the operation back onto the main thread:
DispatchQueue.main.async {
// do UI work
}

When and where to dismiss UIAlertController in Swift?

I am calling a method that executes a URLSession but before it does anything, presents a UIAlertController blocking the UI until some sort of response from the request is achieved. Logic tells me that dismissing the UIAlertController in the completion block of that method where it is called on the main thread would be the best option. Am I wrong to assume this? Apparently so, as variably the presented UIAlertController will indeed display, but never dismiss. Help?
Block:
getCostandIV { output in
let cost = output["ask"] as! NSNumber
let IV = output["IV"] as! NSNumber
self.enteredCost = cost.stringValue
self.enteredIV = IV.stringValue
DispatchQueue.main.async {
self.progress.dismiss(animated: true, completion: nil)
self.tableView.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.none)
self.canWeSave()
}
}
Function:
func getCostandIV (completionBlock: #escaping (NSMutableDictionary) -> Void) -> Void {
DispatchQueue.main.async {
self.progress = UIAlertController(title: "Retrieving ask price and volatility...", message: nil, preferredStyle: UIAlertController.Style.alert)
self.present(self.progress, animated: true, completion: nil)
}
guard let url = URL(string: "https://api.tdameritrade.com/v1/marketdata/chains?apikey=test&symbol=\(symbol)&contractType=\(type)&strike=\(selectedStrike)&fromDate=\(selectedExpiry)&toDate=\(selectedExpiry)") else {
return
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
//print(error?.localizedDescription ?? "Response Error")
DispatchQueue.main.async {
self.presentedViewController?.dismiss(animated: true, completion: {
let alert = UIAlertController(title: "There was an error retrieving ask price and volatility.", message: "Please try again later.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
})
}
return }
do{
//here dataResponse received from a network request
let jsonResponse = try JSONSerialization.jsonObject(with:
dataResponse, options: [])
// //print(jsonResponse) //Response result
guard let jsonDict = jsonResponse as? NSDictionary else {
return
}
// //print(jsonDict)
var strikeMap : NSDictionary = [:]
if self.type == "CALL" {
strikeMap = jsonDict["callExpDateMap"] as! NSDictionary
} else {
strikeMap = jsonDict["putExpDateMap"] as! NSDictionary
}
self.strikes.removeAllObjects()
let inner = strikeMap.object(forKey: strikeMap.allKeys.first ?? "<#default value#>") as! NSDictionary
let innerAgain = inner.object(forKey: inner.allKeys.first ?? "<#default value#>") as! NSArray
let dict : NSDictionary = innerAgain[0] as! NSDictionary
let dict2 = ["ask" : dict["ask"] as! NSNumber, "IV" : dict["volatility"] as! NSNumber] as NSMutableDictionary
completionBlock(dict2)
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
Edit: Using self.presentedViewController?.dismiss(animated: true, completion: nil) did not fix the issue. In addition, the completion block of the dismiss function for self.progress is not being called.
Edit 2: presentedViewController right before dismiss code in the callback is nil, even though present is called on the alert controller before dismiss?
use this , for dismiss your alert you should add dismiss method in an async block and for setting timer for that you should tell async block to start being async from now to 5 seconds and after that do some thing :
alert.addAction(UIAlertAction(title: "ok", style: .default,
handler: nil))
viewController.present(alert, animated: true, completion: nil)
// change to desired number of seconds (in this case 5 seconds)
let when = DispatchTime.now() + 5
DispatchQueue.main.asyncAfter(deadline: when){
// your code with delay
alert.dismiss(animated: true, completion: nil)
}
If you call the getCostandIV method more than once, the second alert won't be presented and self.progress will have the reference of unpresented alert.
Change
self.progress.dismiss(animated: true, completion: nil)
To
self.presentedViewController?.dismiss(animated: true, completion: nil)
Your alert would be dismissed only if everything goes well.
I suggest you to change your function to something like this:
func getCostandIV (completionBlock: #escaping (NSMutableDictionary?, Error?) -> Void) -> Void
and make sure that your completionBlock is called when your guard statements fail or an error is thrown. In your current code the alert is only dismissed when it network request fails, but not when something goes wrong when parsing JSON.

How to wait a value from api response for login view using swift

I started learning Swift and are not familiar with synchronous and asynchronous operations with swift code.
I want to be in my login view (viewcontroller), when the user enters the ID, PASSWORD, and then requests the service from the API, the API will compare the database data is correct, if the correct return json data => true, the error returns json data => false
I don't know how to make my login() execute after getting the API response.
I have researched a lot of information about this, including NSOperationQueue ... etc.
But the final result of the implementation failed, please help me with experienced people, thank you!
var jsonmessage: Bool? = nil
when onclickLogin will go next page using segue
#IBAction func onclickLogin(_ sender: Any) {
Postusercheck()
login()
}
request api
func Postusercheck(){
let parameters = ["ID":txtcount.text,"Password":txtpaswoerd.text] //post request with id,password
print(parameters)
let url = URL(string: "http://" + apiip + "/api/user")! //change the url
let session = URLSession.shared
//now create the URLRequest object using the url object
var request = URLRequest(url: url)
request.httpMethod = "POST" //set http method as POST
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) // pass
} catch let error {
print(error.localizedDescription)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print(json)
self.jsonmessage = json["message"] as? Bool
print("Title: \(String(describing: self.abc)))")
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
func login(){
if (self.jsonmessage == true){
}
else if txtcount.text == "" || txtpaswoerd.text == "" {
let alert = UIAlertController(title: "Don't empty the field", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
else
{
let alert = UIAlertController(title: "ID OR PASSWORD ERROR", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
}
========================updated========code======
#IBAction func onclickLogin(_ sender: Any) {
Postusercheck()
}
func Postusercheck(){
let parameters = ["ID":txtcount.text,"Password":txtpaswoerd.text] //post request with id,password
print(parameters)
let url = URL(string: "http://" + apiip + "/api/user")! //change the url
let session = URLSession.shared
//now create the URLRequest object using the url object
var request = URLRequest(url: url)
request.httpMethod = "POST" //set http method as POST
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) // pass
} catch let error {
print(error.localizedDescription)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print(json)
self.jsonmessage = json["message"] as? Bool
self.login()
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
func login(){
if (self.jsonmessage == true){
//Navigate to the another view controller
let mainStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let anotherViewController = mainStoryboard.instantiateViewController(withIdentifier: "AnotherViewController")
self.navigationController?.pushViewController(anotherViewController, animated: true)
}
else if txtcount.text == "" || txtpaswoerd.text == "" {
let alert = UIAlertController(title: "Don't empty the field", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
else
{
let alert = UIAlertController(title: "ID OR PASSWORD ERROR", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
}
When I try the new code.
I have canceled the original segue connection. I set the Storboard ID to AnotherViewController on the page I want, but when the account password is successfully verified, he does not go to the next page.
He just stopped at the original page (after the account password verification is successful)
If I enter the wrong account password, it still get an error message. This function is useful.
Make the function call in the completion handler of URLSession like below :
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print(json)
self.jsonmessage = json["message"] as? Bool
self.login()
print("Title: \(String(describing: self.abc)))")
}
} catch let error {
print(error.localizedDescription)
}
})
You have to remove the login call from the button's IBAction cause when you are calling the Postusercheck it is going to make the webservice call and it will not wait for the response as the session.dataTask is asynchronous. So the execution will go back to the IBAction and login() will be called though you have not received the web service response and you don't know if the user name and passwords are correct. So you have to remove login call from the button click action like this :
#IBAction func onclickLogin(_ sender: Any) {
Postusercheck()
}
Change the login function as below :
func login(){
if (self.jsonmessage == true){
//Navigate to the another view controller
let mainStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let anotherViewController = mainStoryboard.instantiateViewController(withIdentifier: "AnotherViewController")
self.navigationController?.pushViewController(anotherViewController, animated: true)
}
else if txtcount.text == "" || txtpaswoerd.text == "" {
let alert = UIAlertController(title: "Don't empty the field", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
else
{
let alert = UIAlertController(title: "ID OR PASSWORD ERROR", message: "Please enter again", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
}
Here make sure that you are having a view controller in the storyboard named "Main" with Storyboard Id as "AnotherViewController" otherwise it won't work.
Another option for navigation is through segues in the storyboards. Below is a great tutorial to learn great things about the storyboard.
https://www.raywenderlich.com/464-storyboards-tutorial-for-ios-part-1
Also one more thing is that Postusercheck() is not so good function name. Please refer below guide line for the best practices in the swift's widely used naming conventions :
https://github.com/raywenderlich/swift-style-guide

URLSession and UI related tasks

I'm trying to authenticate login by retrieving a boolean from my web server using URLSession, and show an Alert Controller if the login fails.
func requestLogin() {
let url = URL(string: "http://mywebserver/login.php")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let postString = "username=\(txtUsername.text!)&password=\(txtPassword.text!)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard data != nil else {
self.promptMessage(message: "No data found")
return
}
do {
if let jsonData = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary {
let success = jsonData.value(forKey: "success") as! Bool
if (success) {
self.dismiss(animated: false, completion: { action in
//Move to next VC
})
return
} else {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "The username or password that you have entered is incorrect. Please try again.")}
)
return
}
} else {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Could not parse JSON!")
})
}
} catch {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Request failed!")
})
}
})
showOverlayOnTask(message: "Logging in...")
task.resume()
}
func promptMessage(message: String) {
let alert = UIAlertController(title: "Login Failed", message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
func showOverlayOnTask(message: String) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
self.present(alert, animated: true, completion: nil)
}
The weird problem I'm getting is that my Logging In alert controller sometimes does not dismiss. It gets stuck until I tap on the screen, which then will dismiss and show the next alert controller. It's very annoying and I don't know where I'm doing wrong.
How do I fix this?
Maybe the problem is that you're trying to dismiss the controller without executing on the main thread, normally the UI changes/updates should be executed on the main thread.
Try this and check if works:
DispatchQueue.main.async {
self.dismiss(animated: false, completion: { action in
self.promptMessage(message: "Error: Could not parse JSON!")
})
}

Swift - Function doesn't wait for results

I've been trying to get information out of a server call. Which already works fine, but when I want to use the information to make a dialogue box appear it doesn't really work. It goes too fast to the next line. The only way I could make it work is by delaying the next bit in the code but considering it's a server call I can't determine how long it'll always take. I tried using dispatch_sync but it didn't work at all.
func logIn(username: String, password: String) -> Void {
var error: NSError?
var call = "api/usersession/"
var login = "username=" + username + "&password=" + password
API().getLogin(login, api:call, { (data) -> Void in
let json = JSON(data: data, error: &error)
println(json.self)
let status = json["status"].stringValue!
if (status == "1") {
alertController = UIAlertController(title: "Something went wrong", message: json["result"].stringValue, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
} else {
alertController = UIAlertController(title: "Welcome!", message: "You're signed in", preferredStyle: UIAlertControllerStyle.Alert)
}
})
}
I believe that whenever the code gets to the "getLogin" part it just immediately goes on to return whatever it has (which is always nothing). When I say that self.presentViewController needs to delay by an amount then it does work, but this would make the app feel a bit clunky.
Edit:
func getLogin(login: String, api: String, success: ((apiData: NSData!) -> Void)) {
var request = NSMutableURLRequest(URL: NSURL(string: website + api)!)
var learn = API()
var postString = login
request.HTTPMethod = "POST"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
learn.httpRequest(request, callback:{(data, error) -> Void in
if let urlData = data {
success(apiData: urlData)
}
})
}
And httpRequest:
func httpRequest(request: NSURLRequest!, callback: (NSData?,
String?) -> Void) {
var configuration =
NSURLSessionConfiguration.defaultSessionConfiguration()
var userPassWordString = apiusername + ":" + apipassword
let userPasswordData = userPassWordString.dataUsingEncoding(NSUTF8StringEncoding)
let base64EncodedCrendtial = userPasswordData!.base64EncodedStringWithOptions(nil)
let authString = "Basic \(base64EncodedCrendtial)"
configuration.HTTPAdditionalHeaders = ["Authorization": authString]
var session = NSURLSession(configuration: configuration,
delegate: self,
delegateQueue:NSOperationQueue.mainQueue())
var task = session.dataTaskWithRequest(request){
(data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
if error != nil {
callback(nil, error.localizedDescription)
} else {
var result = NSData(data: data)
callback(result, nil)
}
}
task.resume()
}
It goes too fast to the next line.
You need to think about asynchronous operations. When you do something that's going to take time, whether it's making a network request to a server or asking the user to provide some information, you don't just make a function call and expect the results to show up immediately. Instead, you call a function or method that starts the process, and then you go off and do other things until the process completes. For a network request, that usually means that you provide a delegate object or a completion block that gets called when the connection has new information. When dealing with the user, you might put up a dialog box and leave it at that -- the process continues when the user triggers an action by tapping some button or filling in some field.
I managed to fix it by making the UIAlertController present itself from the function itself like so:
if (status == "1") {
alertController = UIAlertController(title: "Er ging iets fout", message: json["result"].stringValue, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController,animated: true, completion:nil)
} else {
alertController = UIAlertController(title: "Welcome!", message: "You're signed in!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController,animated: true, completion:nil)
}

Resources