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
Related
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).
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 2 view controllers, in the first one I make calculations that repeat in an endless loop.
The problem is that I want to close the method and everything related to the first method when presenting the second one. Also I am conforming to MKMapViewDelegate that is triggered everytime that user location changes, where I start a background thread work.
Now when presenting the other view controller, I want to get rid of this and break all the operations that were being executed.
I tried to set the delegates to nil, but when turn back to the first one the methods return and gives crash by saying
"Collection <__NSArrayM: 0x17f9b100> was mutated while being enumerated."
This is the function where I make calculations, the array has too many objects in it and takes about 10 sec to fully check this method.
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self makeCalculations];
});
}
-(void)makeCalculations {
BOOL okClicked = NO;
for(NSDictionary *item in array) {
NSInteger responseCode = [[item objectForKey:#"responseCode"] integerValue];
okClicked = (responseCode > 0);
if (okClicked) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"title" message:#"message" delegate:self cancelButtonTitle:#"OK" otherButtonTitles: nil];
alert.tag =10;
[alert show];
});
}
}
}
Is there any clue and can you provide me an example or suggestion?
Keep a counter and increment it in the loop. Then use something like:
if(counter % 100 == 0) {
if(self.cancelled) {
self.cancelled = NO;
return;
}
}
Now, just set the cancelled BOOL when you present the new modal.
You don't strictly need the counter, you could just check the flag each time...
I want to make all reading/writing database operations to background queue and update the current UI view when completed.
There is no problem if user stays in the view while I'm dealing with my database. However, if user left that view before database operations completed, it would crash. The psuedo code is as below:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
/* save data to database, needs some time */
dispatch_async(dispatch_get_main_queue(), ^{
// back to main queue, update UI if possible
// here may cause crash
[self.indicator stopAnimating];
[self.imageView ...];
});
});
Try checking if the view is still in the view hierarchy, and also stop the activity indicator from spinning in the viewDidDisappear method as well. You also might need a flag (isNeedingUpdate in the example below) to indicate whether the UI was updated or not, so you can do the appropriate actions if the user goes away before the update is complete and then comes back again.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
if (self.view.window) { // will be nil if the view is not in the window hierarchy
dispatch_async(dispatch_get_main_queue(), ^{
[self.indicator stopAnimating];
[self.imageView ...];
self.isNeedingUpdate = NO;
});
}else{
self.isNeedingUpdate = YES;
});
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (isNeedingUpdate) {
// do whatever you need here to update the view if the use had gone away before the update was complete.
}
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.indicator stopAnimating];
}
I have the following code:
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Loading Content For the First Time..."
message:#"\n"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.center = CGPointMake(139.5, 75.5); // .5 so it doesn't blur
[alertView addSubview:spinner];
[spinner startAnimating];
[alertView show];
for (TCMLevelRemote *level in [obj objectForKey:#"levels"]){
[[TCMExhibitFeedStore sharedStore] createLevel:level];
}
[[TCMExhibitFeedStore sharedStore] loadAllLevels];
[[TCMExhibitFeedStore sharedStore] setAllLevels:[[TCMExhibitFeedStore sharedStore] storedLevels]];
[alertView dismissWithClickedButtonIndex:0 animated:YES];
The for loops takes a while to execute because it downloads some information for the first time the app runs. So I want this notification to show so the user doesn't sit waiting at an unresponsive screen. The problem is the alertview doesn't show until the for loop ends. Then it just goes away right away. What do I need to change?
Declare your alert-view Object in .h class for using everywhere in .m class.
Put your for-loop code in performSelectorInBackground for running loop in Backgroud so you Alertview Not waiting for Your ForLoop finishing.
[self performSelectorInBackground: #selector(LoadForLoop) withObject: nil];
-(void)LoadForLoop
{
for (TCMLevelRemote *level in [obj objectForKey:#"levels"]){
[[TCMExhibitFeedStore sharedStore] createLevel:level];
}
[[TCMExhibitFeedStore sharedStore] loadAllLevels];
[[TCMExhibitFeedStore sharedStore] setAllLevels:[[TCMExhibitFeedStore sharedStore] storedLevels]];
[alertView dismissWithClickedButtonIndex:0 animated:YES];
}
Other solution
You can also use Grand Central Dispatch (GCD) like bellow as per your code:-
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Loading Content For the First Time..."
message:#"\n"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.center = CGPointMake(139.5, 75.5); // .5 so it doesn't blur
[alertView addSubview:spinner];
[spinner startAnimating];
[alertView show];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (TCMLevelRemote *level in [obj objectForKey:#"levels"]){
[[TCMExhibitFeedStore sharedStore] createLevel:level];
}
[[TCMExhibitFeedStore sharedStore] loadAllLevels];
[[TCMExhibitFeedStore sharedStore] setAllLevels:[[TCMExhibitFeedStore sharedStore] storedLevels]];
dispatch_async(dispatch_get_main_queue(), ^{
[spinner StopAnimating];
[alertView dismissWithClickedButtonIndex:0 animated:YES];
});
});
I think what you're looking for is to create your 'levels' while your alert view shows an activity indicator to the user.
Right now you're running your for loop on the same thread as your UI code. Your code will run in sequence, line after line. On iOS and Mac OS, the thread's run loop must be given room to breath to allow for render and timing events, in this case for animation. By blocking the run loop until your for loop ends, your UIAlertView will not have time to animate itself in until after the loop, and then your dismissWithClickedButtonIndex:animated: call immediately hides it.
What you want to do is shift your processing to the background, using something like Grand Central Dispatch:
// Perform all of your UI on the main thread:
// ... set up your alert views, etc
// Then shift your logic to a background thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// This block is executed on a background thread, so will not block the UI:
for (TCMLevelRemote *level in [obj objectForKey:#"levels"]){
[[TCMExhibitFeedStore sharedStore] createLevel:level];
}
[[TCMExhibitFeedStore sharedStore] loadAllLevels];
[[TCMExhibitFeedStore sharedStore] setAllLevels:[[TCMExhibitFeedStore sharedStore] storedLevels]];
// Finally, now that your background process is complete, you can update the interface accordingly by dismissing the alert view:
dispatch_async(dispatch_get_main_queue(), ^{
[alertView dismissWithClickedButtonIndex:0 animated:YES];
});
});
When dealing with background threads, it is important to note that UI events must be performed back on the main thread.
I like to package my tasks up into NSOperation subclasses, which both helps to separate the UI from the model logic, and also handles GCD for me. I'll leave that as an exercise for you.
A side note regarding your choice of UI: alert views are not meant for notifying the user of some process. They are meant for alerting the user that a single event has occurred. I would instead advise the use of something like MBProgressHUD, especially as it has built in support for GCD methods with doSomethingInBackgroundWithProgressCallback:completionCallback:.
The alert view is not displayed because with your loop you are blocking the main thread, which is the thread that must draw the alert view.
In the place where you originally have your code, write this:
// Your original code that creates and sets up the alertView
UIAlertView* alertView = ...
// Add this snippet
NSTimeInterval delay = 0.1; // arbitrary small delay
[self performSelector:#selector(delayedLoop:) withObject:alertView afterDelay:delay];
// Show the alert. Because the delayedLoop: method is invoked
// "a little bit later", the main thread now should be able to
// display your alert view
[alertView show];
Add this method to your class:
- (void) delayedLoop:(UIAlertView*)alertView
{
// Add your code that runs the loop and dismisses the alert view
}
This solution is a bit "hackish", but since your loop is still running in the main thread context you won't have any threading problems. If you are willing to execute your loop in a secondary thread, then you should have a look at Nithin Gohel's answer.