I have a situation where I want to present a UIAlertController in order to wait for an event (asynchronous request for data from third party) to finish before showing my main ViewController to the user to interact with.
Once the asynchronous code finishes, then I want to dismiss the UIAlertController. I know that normally UIAlertControllers are setup with a button to dismiss it, which is input from the user. I am wondering if what I want to do (dismiss with an event instead of user input) is possible?
So far, I tried to show the UIAlertController, and then wait in a while loop checking a boolean for when the event occurs:
var alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: UIAlertControllerStyle.Alert)
self.presentViewController(alert, animated: true, completion: nil)
// dataLoadingDone is the boolean to check
while (!dataLoadingDone) {
}
self.dismissViewControllerAnimated(true, completion: nil)
This gives a warning
Warning: Attempt to dismiss from view controller while a presentation or dismiss is in progress!
and does not dismiss the UIAlertController. I also tried alert.dismissViewControllerAnimated(true, completion: nil) instead of self.dismissViewControllerAnimated(true, completion: nil), but this doesn't get rid of the UIAlertController either.
I wouldn't use a while loop but a didSet observer for your dataLoadingDone property. Thereby, you may try something similar to the following code:
class ViewController: UIViewController {
var dismissAlertClosure: (() -> Void)?
var dataLoadingDone = false {
didSet {
if dataLoadingDone == true {
dismissAlertClosure?()
}
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: .Alert)
presentViewController(alert, animated: true, completion: nil)
// Set the dismiss closure to perform later with a reference to alert
dismissAlertClosure = {
alert.dismissViewControllerAnimated(true, completion: nil)
}
// Set boolValue to true in 5 seconds in order to simulate your asynchronous request completion
var dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(5.0 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), { self.dataLoadingDone = true })
}
}
Another user (matt) gave what I'm pretty sure is the correct answer, but he removed it, so I'm going to answer it. Just wanted to give credit (though he seems pretty reputable anyways).
What he wrote is that my UIAlertController presentation was not finished before I tried to dismiss it, so I got that error. I changed my code to the following:
// check if I need to wait at all
if (!dataLoadingDone) {
var alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: UIAlertControllerStyle.Alert)
self.presentViewController(alert, animated: true, completion: { () -> Void in
// moved waiting and dismissal of UIAlertController to inside completion handler
while (!self.dataLoadingDone) {
}
self.dismissViewControllerAnimated(true, completion: nil)
})
}
I moved the waiting and dismissal inside the completion handler of the presentViewController call, so that I know the presenting is done before dismissing. Also, I check if I need to wait at all with the first if statement, because otherwise another issue occurs if the while loop never does anything.
I'm not 100% sure yet, because my boolean is actually never false at the moment (the data retrieval happens really quickly). However, I have reason to believe it will take longer later on in my app development, so I will update the answer here once I have that.
EDIT: another user who previously posted an answer (Aaron Golden) was also correct about while loop blocking. I had thought that the while loop shares processing time with other events, but apparently not (or at least not enough time). Therefore, the above while loop DOES NOT work
Related
I have a game which displays an alert whenever a player wins. However after restarting the game and the same alert shows up multiple 'cancel' buttons show up. just like showed in the picture. any ideas what it could be,
var alertX = UIAlertController(title: "Winner", message: "X Has Won", preferredStyle:.alert)
func AlertPlayer1() {
alertX.addAction(UIAlertAction(title:"CLOSE",style: UIAlertAction.Style.destructive, handler: { (action) in self.alertX.dismiss(animated: true, completion: nil)}))
self.present(alertX, animated:true, completion:nil)
}
I have simply then just called the function whenever somebody wins
Please update your code as following to fix issue.
func AlertPlayer1() {
var alertX = UIAlertController(title: "Winner", message: "X Has Won", preferredStyle:.alert)
alertX.addAction(UIAlertAction(title:"CLOSE",style: UIAlertAction.Style.destructive, handler: { (action) in
self.alertX.dismiss(animated: true, completion: nil)
}))
self.present(alertX, animated:true, completion:nil)
}
You are creating alert instance single time, but this method AlertPlayer1 call multiple time from somewhere in your code which are adding multiple close button.
Note: As per I already told you, you method calling multiple time. So this alert also try to present multiple time, but at a time your can present only one view controller in window/screen. So it will show you warning in console.
I have a login screen that raises an alert when login fails. The code that calls the alert is run from a callback closure which itself calls a function in the main VC.
_ = UserProfile.logIn(emailAddressLabel.text!, passwordLabel.text!) { success in
if success {
self.performSegue(withIdentifier: "mainTabBarSegue", sender: nil)
}
else {
self.displayFailedLoginAlert()
}
self.loggingIn = false
}
and the displayFailedLoginAlert looks like this:
func displayFailedLoginAlert () {
let alert = UIAlertController(title: "Login", message: "Login Failed", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default, handler: { _ in
alert.dismiss(animated: false, completion: nil)
}))
self.present(alert, animated: false, completion: nil)
}
However, when I do this I get:
Warning: Attempt to present <UIAlertController: 0x7ff8fd0b5800> on <LoginViewController: 0x7ff8fcc0deb0> which is already presenting <UIAlertController: 0x7ff8fe0cca00>
I have tried a number of different approaches and either get this or a crash if I use a UIAlertController as a class member. What am I doing wrong, I just can't see it?
You don't need to tell the alert to dismiss at all. The default behavior when tapping an action in a UIAlertController is that the alert will dismiss. Just pass nil to the handler.
The issue was that, each time the user logged in I added an observer to the API call. So, on the second login attempt the closure was called twice and so raised the error. Thanks to Frizzo for pointing me in the right direction
I just realized that calling dismiss on alert controllers too early causes them to not get dismissed. For example, if I present the alert controller and then immediately try to dismiss it, the dismission is just ignored. For example
// Done in viewDidLoad
let alertController = UIAlertController(title: nil, message: "Connecting to Bubble Centerpiece...\n\n", preferredStyle: .alert)
present(alertController, animated: true, completion: nil)
alertController.dismiss(animated: true, completion: nil)
With this code, AlertController is not being dismissed. In my case, my dismissed was usually being called in about 0.5 seconds after the alert controller was presented and it did not get dismissed. I had to manually delay the dismission code like this to make it work.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { self.alertController.dismiss(animated: true, completion: nil)})
My assumption is that alert controller takes some time to be set up properly and if the dismiss call arrives earlier than the alert is actually displayed, then it will not be dismissed. I was wondering if there is a more elegant solution to this instead of just delaying it with DispatchQueue.
Because, as long as you pass animated: true, the alert controller will not be in the hierarchy until it is done animating, so you can't dismiss it until then. This is precisely what the completion block is used for (generally, any good API where things happen asynchronously will provide you a completion block to let you know when that action is complete). You can dismiss immediately after presenting (though I don't imagine this is a valuable real-life use case) by doing:
present(alertController, animated: true, completion: {
alertController.dismiss(animated: true, completion: nil)
})
I have a Parse Sign Up and I have an UIAlertController and I want the UIAlertController to show up with an indicator but get dismissed with another UIAlertController when there is an error in the sign up.
I have several cases for a failed sign up, all work prior to adding the UIAlertController with the indicator. And the indicator alert controller works fine when the sign up is successful and it get dismissed correctly.
//create an alert controller
let pending = UIAlertController(title: "Creating New User", message: nil, preferredStyle: .Alert)
//create an activity indicator
let indicator = UIActivityIndicatorView(frame: pending.view.bounds)
indicator.autoresizingMask = .FlexibleWidth | .FlexibleHeight
//add the activity indicator as a subview of the alert controller's view
pending.view.addSubview(indicator)
indicator.userInteractionEnabled = false // required otherwise if there buttons in the UIAlertController you will not be able to press them
indicator.startAnimating()
self.presentViewController(pending, animated: true, completion: nil)
Here is the code for the sign up, this works
if signUpError == nil {
println("Sign Up Successful")
// Keep track of the installs of our app
var installation: PFInstallation = PFInstallation.currentInstallation()
installation.addUniqueObject("Reload", forKey: "channels")
installation["user"] = PFUser.currentUser()
installation.saveInBackground()
// to stop the uialertviewcontroller once sign up successful
pending.dismissViewControllerAnimated(true, completion: {
self.performSegueWithIdentifier("goToAppFromSignUp", sender: self)
})
}
Here is where I am stuck, this is just one of the cases if I can get this to work I can do it for the others.
else {
println("Error Sign Up")
//If email has been used for another account kPFErrorUserEmailTaken
if(signUpError!.code == 203) {
let alertController = UIAlertController(title: "Sign Up Failed", message: "Sorry! Email has been taken! ", preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK", style: .Default) { (action) in
// ...
}
alertController.addAction(OKAction)
self.presentViewController(alertController, animated: true) {
// ...
}
}
My thought was to dismiss the UIAlertController and show the next one in the completion block
pending.dismissViewControllerAnimated(true, completion: {
self.presentViewController(alertController, animated: true) {
// ...
}
})
But the app freezes on the pending alert controller (the one with the indicator). which is already presenting (null)
Any ideas? Thanks.
Try to dismiss the pending alert and present the next alert without animation.
pending.dismissViewControllerAnimated(false, completion: nil)
self.presentViewController(alertController, animated: false, completion: nil)
In the apple documentation, it states that you should not modify the view hierarchy for UIAlertController. This could cause you problems later and may be part of your current issue.
It is also likely that presenting one while the other is running is causing your issue. Dismissing the first and once complete presenting the second should in theory fix your issue. The fact it hangs suggests something else is happening. You should stop and remove the activity indicator from the view before dismissing it.
A better approach I think would be to consider using a HUD like https://github.com/jdg/MBProgressHUD to display progress or activity rather than trying to use an alert controller for a purpose it was not intended for.
A HUD is great for presenting heads up progress or activity while waiting for things to happen. It has a number of display options including activity with headline text. I would present the HUD when you are waiting and then use the alert controller to present alerts.
I have found using a HUD looks better and is much easier to control.
I'm using the Twitter login in the signup process of my app. And I'm asking for the user's email. Once I get it, I'd like to present a UIAlertController.
Here's my code:
func askForTWMail(){
if (Twitter.sharedInstance().session() != nil) {
let shareMailVC=TWTRShareEmailViewController(completion: {(mail:String!, error:NSError!) in
if (mail != nil) {
print("GOT MAIL: \(mail)")
self.gotMail()
}else{
print("MAIL VC ERROR: \(error)")
}
})
println("PRESENT MAIL VC")
self.presentViewController(shareMailVC, animated: true, completion: nil)
}else{
println("User not logged in")
}
}
func gotMail(){
var alertController=UIAlertController(title: "Some title", message: "Some message", preferredStyle: UIAlertControllerStyle.Alert)
var okAction=UIAlertAction(title:"Yes", style: UIAlertActionStyle.Default) {
UIAlertAction in
//some action
}
var cancelAction=UIAlertAction(title:"No", style: UIAlertActionStyle.Cancel){
UIAlertAction in
//some action
}
alertController.addAction(okAction)
alertController.addAction(cancelAction)
self.presentViewController(alertController, animated: true, completion: nil)
}
But I get this error (I guess because the TWTRShareEmailViewController is not dismissed):
Warning: Attempt to present UIALertController on xViewController whose
view is not in the window hierarchy!
Any idea of how I should write this? How can I know when the TWTRShareEmailViewController is dismissed to continue the signup process and be able to present my UIAlertController? I'm not aware of a delegate method related to TWTRShareEmailViewController.
Any help is appreciated. Thanks.
Found a solution here. I'm probably doing it wrong but if not it might be an Apple bug. The workaround is to delay the presentation of the UIAlertController:
dispatch_async(dispatch_get_main_queue(), ^{
self.presentViewController(alertController, animated: true, completion: nil)
})
EDIT: I found another workaround (I don't use the solution I put down here anymore). I had to change this because the Twitter login was also breaking my transitions between VCs.
I now call a specific UIViewController (I called it something like TWLoginVC) where I do all the Twitter login and other stuff. The view is just black so the user don't see the process is actually done in another VC (he just has to pick up the Twitter user he wants to login with). I guess you could also put a clear background to be even more invisible.
When I call this view controller and dismiss it, the transition is not applied to it and I don't have any more problem with it.
EDIT
Update for Swift:
DispatchQueue.main.async{
self.present(alertController, animated: true, completion: nil)
}
Here's an updated answer for Swift 3 tested on Xcode 8 based on Marie Dm's answer.
DispatchQueue.main.sync {
self.present(alertController, animated: true, completion: nil)
}
If DispatchQueue.main.sync caused crush, try DispatchQueue.main.async.
"async" worked for my problem while returning from contactPicker().
delay presenting viewController solved my problem and saved my day :D
Swift 4.0 :
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
//present your view controller her
self.presentViewController(alertController, animated: true, completion: nil)
}