Allow User to Cancel MBProgressHUD when JSON call takes too long - ios

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.

Related

Weird delay in UIPopoverController dismissal

Maybe this is purely simulator related. I have not tried it on an actual device yet.
I'm on the latest greatest MacBook with a 1TB flash drive, and 95% free processor, and less than full memory consumption.
I have a UIPopoverController with 4 items in it, sized to those items.
There's nothing complicated or multi-threaded or long running in any way associated in the UIPopoverController in question.
I've set the appear and dismiss animation at 0, yet when I tap on an item in the list, there seems to be an random indeterminate delay between 0 and .4 seconds in the popover disappearing. Of course the 0 is expected, but the times when it's nearly a half second is very noticeably longer and disconcerting.
Any idea what may be causing that?
Code that shows the popover...
-(IBAction)theLandImpsButtonPressed:(UIButton *)sender
{
iRpNameValuePopover *thePopoverContent = [[iRpNameValuePopover alloc] init];
thePopoverContent.theTableValues = [self getLandImpsChoicesList];
impsLandPopover = [[UIPopoverController alloc] initWithContentViewController:thePopoverContent];
thePopoverContent.thePopoverController = impsLandPopover;
impsLandPopover.popoverContentSize = [iRpUIHelper sizeForPopoverThatHasTitle:NO andListContent:thePopoverContent.theTableValues];
impsLandPopover.delegate = self;
[impsLandPopover presentPopoverFromRect:self.theLandImpsButton.bounds inView:self.theLandImpsButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO];
}
Code that dismisses the popover...
BTW, there is no evaluation time incurred here [self userChoiceIsValid] because it simply returns YES right now.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
_theChosenNameValueItem = [self.theTableValues objectAtIndex:indexPath.row];
[self acceptUserChoiceAndClose];
}
// This contentViewController is encapsulated INSIDE a UIPopoverViewController, and this class cannot itself
// close the popover which contains it, hence the need for the reference to the popover controller
// It is the popover's delegate... the one that created the popover, that is able to close it.
-(void)acceptUserChoiceAndClose
{
_theUserChoseAValue = NO; // Start by assuming they didn't chose a valid value.
if ([self userChoiceIsValid])
{
// Set variable that indicates the user chose a value which can be saved to core data, and/or presented on screen.
_theUserChoseAValue = YES;
// Close the popover.
[_thePopoverController dismissPopoverAnimated:NO];
// Notify the class that presented the popover that the popover has been dismissed.
// It will still be available to the dismissal method where code can retrieve the user's choice, and set the popover to nil.
if (_thePopoverController.delegate && [_thePopoverController.delegate respondsToSelector:#selector(popoverControllerDidDismissPopover:)])
{
[_thePopoverController.delegate popoverControllerDidDismissPopover:_thePopoverController];
}
}
else
{
[self showValidationFailureMessageToUser];
}
}
Dismissing the viewController in main thread will solve the issue.
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
I would check it out in the profiler and see what the time is being spent on.
There's a good tutorial here.
UIPopoverPresentationController *popOverView;
//////
Dismiss it...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
[popOverView.presentedViewController dismissViewControllerAnimated:NO completion:nil];
popOverView = nil;
});
});

UIAlertView reloads Views that were removed from their superView

I alloc and show an UIAlertView ( standard one button ). On
- (void)willPresentAlertView:(UIAlertView *)alertView{
NSArray *a = [self.view subviews];
}
I get one element. The view that was in effect when I showed the Alert. On
- (void)didPresentAlertView:(UIAlertView *)alertView{
NSArray *a = [self.view subviews];
}
I get 4 elements(??). UIAlertView somehow brings to life views that I previously have removed from their superview. Why??
Thanks, David
Showing an alert is an activity related to UI and it's generally good practice to perform all the UI related tasks on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
});
Inside the dispatch block, you should display your alert.
Hope this helps..

UIAcitivity Indicator with UIAlertView

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.

iOS GUI refresh

I am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.

MBProgressHUD blocks interactions with an uiscrollview when shown

I'm using this MBProgressHUD code:
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.mode = MBProgressHUDModeText;
hud.removeFromSuperViewOnHide = YES;
[hud hide:YES afterDelay:1];
For the period that the HUD is being shown interactions with an UIScrollview (that contains the button this is called from) are disabled. I can't click on other buttons, or scroll the UIScrollview.
Why is MBProgressHUD blocking my interactions with the UIScrollview and how can I disable it?
I'm using MBProgressHUD version 0.5 and simply set:
HUD.userInteractionEnabled = NO;
With this allow user interaction in parent view.
maybe because the buttons are in the same view what you are trying to add the progress view and when the progress view is in view this view blocks the view what the buttons are added.
MBPregressHUD blocks the interaction in below method
-(void)show:(BOOL)animated{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
...
...
...
}
This line of code blocks all of the interactions inside your application.
If you do want to keep the interactions, drive it through a class level BOOL, you can call it isModal and decide if you want to block the interactions or not
-(void)show:(BOOL)animated{
if(isModal){
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
}
...
...
...
}
Ideally you should create a new 'init' method to take this BOOL as parameter. Possible signature could be
- (id)initWithView:(UIView *)view isModal:(BOOL)modal;
and then initialize the class level BOOL inside this method after initializing the view.
The same condition should be applied in the 'hide' method.
Happy coding... :)

Resources