When and where to dismiss UIAlertController in Swift? - ios

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.

Related

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

Swift Firebase Append Image for UIActivityViewController

I need help appending a image I downloaded from firebase so I can use it to be share over the UIActivityViewController. I get the right image but unsure how to set up the data pass to let it be shared. The current set up throws a "Thread 1 error" on "objectsToShare = [self.image!]".
let shareAction = UIAlertAction(title: "Share", style: UIAlertActionStyle.default, handler: {(alert: UIAlertAction!) in
var objectsToShare: [AnyObject]?
let titlePost = self.feeds[sender.tag].downloadURL
if let postURL = URL(string: titlePost) {
let postRequest = URLRequest(url: postURL)
self.image?.setImageWith(postURL, placeholderImage: nil, options: SDWebImageOptions.progressiveDownload, completed: { (imageRequest, imageResponse, error) -> Void in
// failure downloading image
print("Error downloading Firebase post image")
print(error)
})
}
objectsToShare = [self.image!]
let activityViewController = UIActivityViewController(activityItems: objectsToShare!, applicationActivities: nil)
// present the view controller
self.present(activityViewController, animated: true, completion: nil)
})
In place of the below:
objectsToShare = [self.image!]
You have to use like this :
if let image = self.image {
objectsToShare.append(image)
}
You should check what a completion handler is first.
You should check for error in the completion, and if there's not, do what you have to do with your image. As the request is asynchronous the image isn't there yet on the line just after.
Do something like that :
self.image?.setImageWith(postURL, placeholderImage: nil, options: SDWebImageOptions.progressiveDownload, completed: { (imageRequest, imageResponse, error) -> Void in
// This closure is called when the request is done
if error == nil {
objectsToShare = [self.image!]
let activityViewController = UIActivityViewController(activityItems: objectsToShare!, applicationActivities: nil)
// present the view controller
self.present(activityViewController, animated: true, completion: nil)
} else {
// failure downloading image
print("Error downloading Firebase post image")
print(error)
}
})

How to use a UIAlertController in MVVM?

I have a VC with code to show an alert:
func showMessage() {
let alertView = UIAlertController(title: "TEST",
message: self.loginViewModel.errorText,
preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil))
present(alertView, animated: true, completion: nil)
}
and I have this login logic in my viewModel which needs to trigger this function:
func submitLoginRequest(userLogin: String, loginPassword: String, loginSecret: String, deviceToken: String) {
let userLogin = UserServices.init()
manager.userServicesApiRequest(url: Endpoints.login, request: userLogin) { (data, error) in
if let data = data {
let status = data["status"].stringValue
if status == "success" {
guard let userObject = UserProfileModel.init(data) else { return }
let encodedUserObject: Data = NSKeyedArchiver.archivedData(withRootObject: userObject)
UserDefaults.standard.set(encodedUserObject, forKey: "userProfile")
print("Login Succeeded") self.coordinatorDelegate?.loginViewModelDidLogin(viewModel: self)
} else {
self.errorText = data["reason"].stringValue
// here is where the controller needs calling!
}
}
I wanted to know how i should have them interact correctly to trigger the VC when the VM case is hit?

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

Memory leak in app extension with UIAlertAction attached to UiViewController

I am developing an app extension that takes an url and upload it to a web service.
If there are errors in the upload request, an Alert should pop up and when the user dismisses it, the extension should complete.
Profiling this code with instruments show a memory leak with two NSISLinearexpression objects.
I found that the incriminating code is found in the UIAlertAction that dismisses the alert: without an action attached to the alert the leak disappear.
I'm supposing for some reason calling:
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
causes troubles with the dismiss of the UIAlertController.
Why is that happening?
Here is my code:
import UIKit
import Social
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchStuff()
}
private func sendAlert(alertMessage:String) {
print("alerting")
let alert = UIAlertController(title: "Send video to Kodi", message: alertMessage, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.default) {
UIAlertAction in
print("Cancel Pressed")
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
private func fetchStuff() -> Void {
print("fetching")
guard let extensionItem = extensionContext?.inputItems[0] as? NSExtensionItem else {
print("Unable to get extensionItem")
return
} // check for only 1 attachment
let itemProvider = extensionItem.attachments as! [NSItemProvider]
let item = itemProvider.first
if (item?.hasItemConformingToTypeIdentifier("public.url"))! {
item?.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler:
{ [weak self] (item: NSSecureCoding?, error: Error?) -> Void in
if let url = item as? NSURL {
print(url.absoluteString!)
self?.sendAlert(alertMessage: "test")
}
})
}
else {
return
}
return
}
}
I just had a similar issue.
The cause of the issue for me was that the CoreData manager we built worked off the main dispatch queue. So when the core data manager called our completion block it was actually on a different queue. I added:
DispatchQueue.main.async { }
Around my dialog creation and present calls and the leak disappeared. Hope it helps :)

Resources