I need to programmatically dismiss a UIAlertController that I'm using as a "please wait" message. I can present the alert without problem but when it comes to dismissing the alert, 50% of the time it dismisses and the other 50% it doesn't, forcing me to restart the app just to continue using it. Any ideas how to dismiss the alert with 100% consistency?
//loadingAlert is a UIAlertController declared in the .h file
//present the Alert
loadingAlert = [UIAlertController alertControllerWithTitle:#"Loading..." message:#"Please wait while we fetch locations" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:loadingAlert animated:YES completion:nil];
//parse JSON file
_listOfAcquisitions = nil;
MNSHOW_NETWORK_ACTIVITY(YES);
NSString *WebServiceURL = [NSString stringWithFormat:#"JSON URL", _search];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSDictionary *dictionary = [JSONHelper loadJSONDataFromURL:WebServiceURL];
dispatch_async(dispatch_get_main_queue(), ^{
_listOfAcquisitions = [NSMutableArray array];
for (NSDictionary *oneEntry in dictionary) {
Acquisitions *acqu = [[Acquisitions alloc] init];
if([oneEntry objectForKey:#"ADDRESS1"] == (NSString *)[NSNull null]){acqu.ADDRESS1 = #"";}
else {acqu.ADDRESS1 = [oneEntry objectForKey:#"ADDRESS1"];}
if([oneEntry objectForKey:#"STATEABBR"] == (NSString *)[NSNull null]){acqu.STATEABBR = #"";}
else {acqu.STATEABBR = [oneEntry objectForKey:#"STATEABBR"];}
if([oneEntry objectForKey:#"TOWN"] == (NSString *)[NSNull null]){acqu.TOWN = #"";}
else {acqu.TOWN = [oneEntry objectForKey:#"TOWN"];}
if([oneEntry objectForKey:#"ZIPCODE"] == (NSString *)[NSNull null]){acqu.ZIPCODE = #"";}
else {acqu.ZIPCODE = [oneEntry objectForKey:#"ZIPCODE"];}
[_listOfAcquisitions addObject:acqu];
}
dispatch_async(dispatch_get_main_queue(), ^{
MNSHOW_NETWORK_ACTIVITY(NO);
[self refreshAnnotations:self];
});
});
});
//finally dismiss the alert...
[loadingAlert dismissViewControllerAnimated:YES completion:nil];
}
I've just been learning how to do this.
So, wherever the alert controller is built, you need to add the action button either to "OK" in default style or "Cancel" in cancel style.
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:#"You made a mistake."
message:#"Pray we don't alter the alert further"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okayAction = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[alertController dismissViewControllerAnimated:YES completion:nil];
}];
[alertController addAction:okayAction];
[self presentViewController:alertController animated:YES completion:nil];
There are other UIAlertActionStyle enumerations, such as UIAlertActionStyleCancel which will put a separator space between other actions and UIAlertActionStyleDestructive which will make the font red but will be in line with other UIAlertActions.
Make sure you add in order: standard actions (Okay, Open Camera, Photo Library) and THEN cancel actions.
There's also preferredStyle:UIAlertControllerStyleActionSheet which is used to set up options for the user. I use this to show Camera and Photo Library.
Also, for your specific dismiss action. The reason it's not working is because you are attempting to dismiss it on the background thread. You should ALWAYS dismiss or make UI changes on the foreground thread. This will cause an NSException/crash in the future.
dispatch_async(dispatch_get_main_queue(), ^{
// dismiss your UIAlertController
});
This is how you should be doing it with your specific code:
dispatch_async(dispatch_get_main_queue(), ^{
MNSHOW_NETWORK_ACTIVITY(NO);
[self refreshAnnotations:self];
[loadingAlert dismissViewControllerAnimated:YES completion:nil];
});
});
You have other issues with your code that you should ask others for help about. I am only a junior developer so I'm not sure how to correctly do what you're trying to do but this should help with dismissing.
You might want to look into loading wheels or toast messages that will say "Please wait" or "Loading".
Using a UIAlertController to show a loading message is rather bad taste.
First of all your network call should probably happen in the completion block of the presentViewController. So you don't dismiss it before it has appeared.
Also the nested dispatch_async seems off, since you call dispatch_get_main_queue(line 32) while already within the mainQue(line 13). And if this were to work the dismiss would need to be within the dispatch_async block so that it actually would dismiss.
But more importantly this is kind of a misuse of the UIAlertController API. Those are intended for user input not to Block UI.
You are better of implementing your own custom view subclass.
Or using MBProgressHUD (https://github.com/jdg/MBProgressHUD). There you can use either the MBProgressHUDModeIndeterminate or MBProgressHUDModeText to accomplish what you are trying.
You're creating/starting/dismissing the alert all within the same block, which means both presentViewController:loadingAlert and [loadingAlert dismissViewControllerAnimated: are being called in the same runloop. This is why you're getting unexpected results.
The dismiss needs to be called from a different cycle of the runloop, so having it called in a separate *block is what you want. You're already doing things in different threads using dispatch_async, which execute in discretely separate runloop so the solution for you is to put the dismissViewControllerAnimated: call within the dispatch_async(dispatch_get_main_queue() to ensure that its both called on the main thread for UI updates, and called in a separate run-loop as its presentation.
You could use dismissWithClickedButtonIndex:animated: but it's now deprecated in iOS9. I would use a timer:
// 3 second delay
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(dismissAlert) userInfo:nil repeats:NO];
- (void) dismissAlert {
[loadingAlert dismissWithClickedButtonIndex:0 animated:YES];
}
Of course, this isn't what UIAlertViews are intended for. You'd be better of using a loading spinner somewhere (UIActivityIndicatorView).
Related
What I need to do is present an alert controller with a progressBar. Once the download is complete, I need to transition to a secondary alert controller with a Success! message and an "Ok" button for the user to just exit this setup. I have built the alerts separately and they are working well on their own (well the progress bar is kinda working...) but with this code:
I expect that when the progressBar hits 100% the first alert controller will be dismissed and the next alert will show up, but nothing is happenning This is my code:
-(void)settingUpToolsProgressPopUp {
UIAlertController* progressAlert = [UIAlertController alertControllerWithTitle:#"Setting up tools ..." message:#"This could take a few minutes. Make sure to keep your tools near your mobile device." preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:progressAlert animated:YES completion:^{
//Progress bar setup
UIProgressView *progressView;
progressView = [[UIProgressView alloc]initWithFrame:CGRectMake(8.0, 98.0, progressAlert.view.frame.size.width - 16, 100.0)];
[[progressView layer]setCornerRadius:50];
progressView.trackTintColor = [UIColor whiteColor];
progressView.progressTintColor = [UIColor blueColor];
progressNumerator = 1.0;
progressDenominator = 1.0;
currentProgress = (float)(progressNumerator/progressDenominator);
[progressView setProgress: currentProgress animated:YES];
[progressAlert.view addSubview:progressView];
if(currentProgress > 1.0){
[self settingUpToolsProgressPopUp];
} else if(currentProgress == 1.0){
[self dismissViewControllerAnimated:YES completion:nil];
[self successAlertPopUp];
}
}];
}
p.s. I know that the values are hardcoded right now... but regardless of any values I use the transition doesn't happen. I don't have access to the updating values yet, so I can't use other values right now... but I would expect that if I am using 100% value, then the transition would happen anyway?
Can anyone point me int he right direction? Why isn't this code working for transitioning between these controllers?
Thanks so much!
You need to call successAlertPopUp method inside dismissViewControllerAnimated's completion handler:
[self dismissViewControllerAnimated:YES completion:^{
[self successAlertPopUp];
}];
Inside of a forin loop, I need to present an UIAlertController and wait for user confirmation before presenting the next one. I presented them inside a forin loop, but only the first one appears(after confirmation, the others don't show up).Any help would be greatly appreciated.
You can use the UIAlertController delegate when a button is pressed you show the next alert.
Make a global alert index:
NSUInteger alertIndex = 0;
Make a global NSArray with your alert details in a NSDictionary eg:
self.alerts = #[#{#"title":#"Alert", #"message":#"Message 1"}, #{#"title":#"Alert", #"message":#"Message 2"}];
Call your first alert with an index, eg:
...title:self.alerts[alertIndex][#"title"]...
and
...message:self.alerts[alertIndex][#"message"]...
In your alert controller delegate's didClickButtonAtIndex:
alertIndex++;
// call your next alert here, explained above.
I need to do something similar to display hints to users throughout my app.
Just managed to cobble this together and it works quite well and is easy to adapt and add to ...
/////
// Alert Properties in #interface
////
#property NSOperationQueue *queue;
#property NSMutableArray* operationArray;
#property int operationCounter;
#property int operationTotal;
/////
// Alert Properties End
////
/////////
// Multi Alert Code Start
////////
- (void) alertCodeWithTitleString: (NSString*) titlestring AndMessageString:(NSString*)messagestring {
NSOperation *tempoperation = [NSBlockOperation blockOperationWithBlock: ^(void) {
NSString* tutTitleString = titlestring;
NSString* tutMessageString = messagestring;
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController * alert= [UIAlertController
alertControllerWithTitle:tutTitleString
message:tutMessageString
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* dontshow = [UIAlertAction
actionWithTitle:#"Don't Show This Again"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
[alert dismissViewControllerAnimated:YES completion:nil];
self.operationCounter++;
[self alertOperationCallMethod2];
}];
UIAlertAction* done = [UIAlertAction
actionWithTitle:#"Close"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
[alert dismissViewControllerAnimated:YES completion:nil];
self.operationCounter++;
[self alertOperationCallMethod2];
}];
[alert addAction:done];
[alert addAction:dontshow];
[self presentViewController:alert animated:YES completion:nil];
});
} ];
self.operationTotal++;
[self.operationArray addObject:tempoperation];
}
-(void) alertOperationCallMethod1 {
self.operationCounter = 0;
self.operationTotal = 0;
self.queue = [[NSOperationQueue alloc] init];
self.operationArray = [[NSMutableArray alloc] init];
[self alertCodeWithTitleString:#"Title1" AndMessageString:#"Message1"];
[self alertCodeWithTitleString:#"Title2" AndMessageString:#"Message2"];
[self alertCodeWithTitleString:#"Title3" AndMessageString:#"Message3"];
// Just keep adding method calls here to add alerts
[self alertOperationCallMethod2];
}
-(void) alertOperationCallMethod2 {
if (self.operationCounter<self.operationTotal) {
[self.queue addOperation:self.operationArray[self.operationCounter]];
}
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
//something here
[self alertOperationCallMethod1];
}
/////////
// Multi Alert Code End
////////
----
All you need to do is add another method call in alertOperationCallMethod1
Multiple versions of alertCodeWith..... method should allow you to customise your alerts to suit.
Hope this helps someone : )
This UIViewController extension does what you want:
The alerts queue up, fifo. I.e. latter alers wait and don't show until
user respond to former alerts.
I am developing an app where the user will get to confirm some action via UIAlertView, if he confirms, I call a method that handles the operation, then I prepare to pop the view I am in to go back to another view after the method has been called.
I want to show UIActivityIndicatorView if the user presses confirm for as long as it takes to execute the method and go to that other view. I used startAnimating and stopAnimating in the proper location, but i never get to see the UI UIActivityIndicatorView shown, not for a sec.
I guess its related to some UI issues due to UIAlertView, not sure if I am correct though. I just need a clue on how to use UIActivityIndicatorView properly for a method execution time.
My code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
self.activityIndicator.alpha = 1.0;
self.activityIndicator.hidesWhenStopped = YES;
self.activityIndicator.center = self.view.center;
[self.view addSubview:self.activityIndicator];
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(buttonIndex == 1) {
[self.activityIndicator startAnimating];
ContactsTableViewController *contactTableView = [self getContactsTVC];
[contactTableView applyActionOnCells];
// doing some setup before poping off to the root view controller of my nav controller
[self.activityIndicator stopAnimating];
// then go to rootViewController
[self.navigationController popToRootViewControllerAnimated:YES];
}
}
I'm not 100% certain, but try to comment out the stopAnimating call and see if it shows up.
If that helps, applyActionOnCells probably blocks your main thread (where all UI stuff also happens) and the indicator never has a chance to show up before you hide it again.
In that case, try do the applyActionOnCells call in the background:
if(buttonIndex == 1) {
[self.activityIndicator startAnimating];
__block typeof(self) bself = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ContactsTableViewController *contactTableView = [bself getContactsTVC];
[contactTableView applyActionOnCells];
dispatch_async(dispatch_get_main_queue(), ^{
[bself.activityIndicator stopAnimating];
// then go to rootViewController
[bself.navigationController popToRootViewControllerAnimated:YES];
});
});
}
Edit: see also an earlier question.
I have a uiView covering the screen with a label and uiindicator that runs when i am doing a network call.
Now here's the problem. I have initialized like this in viewdidload
[self.UiIndicator_view setHidden:YES];
[self.UiIndicator_label setHidden:YES];
[self.UiIndicator_indicator setHidden:YES];
On the network call i have called
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
[self ShowIndicator:#"Syncing Data"];
if([self CheckNetwork])
[HttpMethods GetHeaderDataForAppHttp];
dispatch_async(dispatch_get_main_queue(), ^
{
UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:#"Network Error"
message:#"You have no network connection. Please connect to a network to sync data."
delegate:self
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[alertView show];
[self StopIndicator];
});
});
and the methods are as follows
-(void)ShowIndicator:(NSString*)labelText
{
[self.UiIndicator_view setHidden:NO];
[self.UiIndicator_label setHidden:NO];
[self.UiIndicator_label setText:labelText];
[self.UiIndicator_indicator setHidden:NO];
[self.UiIndicator_indicator startAnimating];
}
-(void)StopIndicator
{
[self.UiIndicator_view setHidden:YES];
[self.UiIndicator_label setHidden:YES];
[self.UiIndicator_indicator setHidden:YES];
[self.UiIndicator_indicator stopAnimating];
}
Now the problem. The code runs perfectly fine when i start the app for first time. The call goes to dispatch_asyn, i see a UIView with label and indicator and after the call returns to main thread, the uiview and indicator disappears and i get the alertview if no data was fetched.
I have given the user an option to manually call the same function if data could not be fetched at first attempt. Here the issue arises, i can see in NSLOG that the method has been called, http request is fired, but i cannot see the uiview with indicator, even the elements and buttons that are supposed to be behind the uiview are not clickable (this means that there is a UIVIEW on top, but i cant see it). after some time i get the uialeart which means the call has completed.
Does setting uiview hidden = yes nullify it or something like that? i am not able to get this simple issue.
You are calling [self ShowIndicator:#"Syncing Data"]; on a background queue.
Only ever update the UI on the Main Queue.
More specifically:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
[self ShowIndicator:#"Syncing Data"]; <<<< THIS IS YOUR PROBLEM
I've read and read on SO about this, and I just can't seem to find anything that matches my situation.
I've got MBProgressHUD loading when the view appears, as my app immediately goes to grab some webservice data. My problem is the back button on my navigationcontroller is unresponsive while the HUD is displayed (and therefore while the app gets its data). I want the user to be able to tap to dismiss (or to be able to hit the back button in the worst case) to get the heck out, if it's an endless wait. Here's my code that runs as soon as the view appears:
#ifdef __BLOCKS__
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
hud.labelText = #"Loading";
hud.dimBackground = NO;
hud.userInteractionEnabled = YES;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// Do a task in the background
NSString *strURL = #"http://WEBSERVICE_URL_HERE";
//All the usual stuff to get the data from the service in here
NSDictionary* responseDict = [json objectForKey:#"data"]; // Get the dictionary
NSArray* resultsArray = [responseDict objectForKey:#"key"];
// Hide the HUD in the main tread
dispatch_async(dispatch_get_main_queue(), ^{
for (NSDictionary* internalDict in resultsArray)
{
for (NSString *key in [internalDict allKeys])
{//Parse everything and display the results
}
}
[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
});
});
#endif
Leaving out all the gibberish about parsing the JSON. This all works fine, and the HUD dismisses after the data shows up and gets displayed. How in the world can I enable a way to stop all this on a tap and get back to the (blank) interface? GestureRecognizer? Would I set that up in the MBProgressHUD class? So frustrated...
Kindest thanks for any help. My apologies for the long post. And for my ugly code...
No need to extend MBProgressHUD. Simply add an UITapGestureRecognizer to it.
ViewDidLoad
:
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:NO];
HUD.mode = MBProgressHUDModeAnnularDeterminate;
UITapGestureRecognizer *HUDSingleTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(singleTap:)];
[HUD addGestureRecognizer:HUDSingleTap];
And then:
-(void)singleTap:(UITapGestureRecognizer*)sender
{
//do what you need.
}
The MBProgressHUD is just a view with a custom drawing to indicate the current progress, which means it is not responsible for any of your app's logic. If you have a long running operation which needs to be canceled at some point, you have to implement this yourself.
The most elegant solution is to extend the MBProgressHUD. You can either draw a custom area which plays the role of a button, add a button programmatically or just wait for a tap event on the whole view. Then you can call a delegate method whenever that button or the view is tapped.
It can look like this:
// MBProgressHUD.h
#protocol MBProgressHUDDelegate <NSObject>
- (void)hudViewWasTapped; // or any other name
#end
// MBProgressHUD.m
// Either this, or some selector you set up for a gesture recognizer
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.delegate respondsToSelector:#selector(hudViewWasTapped)]) {
[self.delegate performSelector:#selector(hudViewWasTapped)];
}
}
you have to set your view controller as the delegate for theMBProgressHUD and act accordingly.
Let me know if you need more clarification on this :)
To have extra information:
You could create contentView in your view
And simply show the hud in your contentView (not in your self.view or self.navigationController.view)
in this way your navigationBar's view will not be responsible for your hudView. So, you can go back from your navigationController's view to previous page.