MBProgressHUD activity indicator doesn't show in viewDidAppear after segue is performed - iphone

I have two UITableViewControllers A and B, and this is what I'm trying to do when I click on a table view cell in A:
Prepare to segue from A to B by setting some of B's variables from A.
Perform segue from A to B.
B appears.
Display a "Loading" activity indicator with [MBProgressHUD][1].
In a background task, retrieve data from a URL.
If an error occurs in the URL request (either no data received or non-200 status code), (a) hide activity indicator, then (b) display UIAlertView with an error message
Else, (a) Reload B's tableView with the retrieved data, then (b) Hide activity indicator
However, this is what's happening, and I don't know how to fix it:
After clicking a cell in A, B slides in from the right with an empty plain UITableView. The MBProgressHUD DOES NOT SHOW.
After a while, the tableView reloads with the retrieved data, with the MBProgressHUD appearing very briefly.
The MBProgressHUD immediately disappears.
There doesn't seem to be an error with the way the background task is performed. My problem is, how do I display the MBProgressHUD activity indicator as soon as my B view controller appears? (And actually, how come it's not showing?) Code is below.
A's prepareForSegue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
B *b = (B *)[segue destinationViewController];
// Set some of B's variables here...
}
Relevant methods in B
- (void)viewDidAppear:(BOOL)animated {
[self startOver];
}
- (void)startOver {
[self displayLoadingAndDisableTableViewInteractions];
[self retrieveListings];
[self.tableView reloadData];
[self hideLoadingAndEnableTableViewInteractions];
}
- (void)displayLoadingAndDisableTableViewInteractions {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = #"Loading";
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
self.tableView.userInteractionEnabled = NO;
}
- (void)hideLoadingAndEnableTableViewInteractions {
[MBProgressHUD hideHUDForView:self.view animated:YES];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.tableView.userInteractionEnabled = YES;
}
- (void)retrieveListings {
__block NSArray *newSearchResults;
// Perform synchronous URL request in another thread.
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
newSearchResults = [self fetchNewSearchResults];
});
// If nil was returned, there must have been some error--display a UIAlertView.
if (newSearchResults == nil) {
[[[UIAlertView alloc] initWithTitle:#"Oops!" message:#"An unknown error occurred. Try again later?" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
} else {
// Add the retrieved data to this UITableView's model. Then,
[self.tableView reloadData];
}
}
- (NSArray *)fetchNewSearchResults {
// Assemble NSMutableArray called newSearchResults from NSURLConnection data.
// Return nil if an error or a non-200 response code occurred.
return newSearchResults;
}

I think you have to call [self hideLoadingAndEnableTableViewInteractions]; after newSearchResults = [self fetchNewSearchResults]; You are retrieving data in another thread which means -startOver will continue executing after calling [self retrieveListings]; and will hide the HUD right away. Also because you are updating the display you have to make sure you are doing that on the main thread. See example
dispatch_async(dispatch_get_main_queue(), ^{
//update UI here
});

When B appears, it displays a plain and empty UITableView, but does not display the MBProgressHUD even if the task does begin in the background (and yet, the MBProgressHUD is called to show before that). Hence, my solution is to show the MBProgressHUD in viewDidLoad, which precedes viewWillAppear.
- (void)viewDidLoad {
// ...
[self displayLoadingAndDisableUI];
}
I set up two additional boolean properties to B--one in .h, called shouldStartOverUponAppearing, and one in a class extension in .m, called isLoadingAndDisabledUI. In startOver, I added the following lines:
- (void)startOver {
if (!self.isLoadingAndDisabledUI) {
[self displayLoadingAndDisabledUI];
}
}
The check is done so that startOver doesn't display another MBProgressHUD when it has already been displayed from viewDidLoad. That is because I have a third view controller, called C, that may call on B's startOver, but doesn't need to call viewDidLoad just to display the MBProgressHUD.
Also, this is how I defined viewDidAppear:
- (void)viewDidAppear:(BOOL)animated {
if (self.shouldStartOverUponAppearing) {
[self startOver];
self.shouldStartOverUponAppearing = NO;
}
}
This way, startOver will only be invoked IF B appeared from A. If B appears by pressing "Back" in C, it will do nothing and only display the old data that was there.
I think that this solution is FAR from elegant, but it works. I guess I'll just ask for a better approach in a separate SO question.

I have used a common method for MBProgressHUD.
#import "MBProgressHUD.h" in AppDelegate.h also following methods.
- (MBProgressHUD *)showGlobalProgressHUDWithTitle:(NSString *)title;
- (void)dismissGlobalHUD;
In AppDelegate.m add following methods.
- (MBProgressHUD *)showGlobalProgressHUDWithTitle:(NSString *)title {
[MBProgressHUD hideAllHUDsForView:self.window animated:YES];
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.window animated:YES];
hud.labelText = title;
return hud;
}
- (void)dismissGlobalHUD {
[MBProgressHUD hideHUDForView:self.window animated:YES];
}
How to use?
AppDelegate *appDel = (AppDelegate *)[[UIApplication sharedApplication]delegate];
//Show Indicator
[appDel showGlobalProgressHUDWithTitle:#"Loading..."];
//Hide Indicator
[appDel dismissGlobalHUD];
Hope this helps.

Related

doubletap on button with callback to segue causes crash

I have a button with this:
- (IBAction)themeBtnAction:(id)sender
{
NSString *language = [[OnlineStore sharedStore]getTheLanguage];
[[OnlineStore sharedStore]getTheThemeBaseGuides:language callback:^{
[self performSegueWithIdentifier:#"from main to themecategory" sender:self];
}]
}
But when user happens to doubletap the button it causes a crash. Probably because next viewcontroller is being loaded twice onto the heap (my guess) and the error message I get when I try to return with UINavigationController pop from that second UIViewControlleris: Unbalanced calls to begin/end appearance transitions for <MySecondViewController: 0xb257b80>.
How can I do to prevent this?
I have tried to put it inside callback within:
if ([NSStringFromClass([[viewControlles lastObject] class]) isEqualToString: #"MainViewController"]) {
and I have tried to do the callback within the selector goToNextView in the btn
[self performSelector:#selector(goToNextView) withObject:self afterDelay:1.0];
No luck.
Any suggestions. And please ask if this is unclear since I am a bit tired and just about to try to sleep now :)
You can ignore user interaction until the view controller disappears:
- (IBAction)themeBtnAction:(id)sender
{
NSString *language = [[OnlineStore sharedStore]getTheLanguage];
[[OnlineStore sharedStore]getTheThemeBaseGuides:language callback:^{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[self performSegueWithIdentifier:#"from main to themecategory" sender:self];
}]
}
- (void)viewDidDisappear:(BOOL)animated
{
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
}
Or just disable user interaction on the button in a similar manner:
button.userInteractionEnabled = NO;
...
button.userInteractionEnabled = YES;

MBProgressHud not hiding after calling hide from another class

I have a ViewController called GetInfoViewController. Basically it takes the users input and then sends the input to a NSObject class, ServerConnection. ServerConnection makes a nsurlconnection request and when it is done I want the MBProgressHUD to hide.
GetInfoViewController
- (IBAction)go:(id)sender{
ServerConnection *serverConnection = [[ServerConnection alloc]init];
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
HUD.labelText = #"Searching...";
// here the progressHUD shows normally, and the yelpConnectionMethod is successful in retrieving the result.
[serverConnection yelpConnectionMethod];
}
-(void)endProgressHUD{
NSLog(#"end called");
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
ServerConnection.h
I am not going to put all the nsurlconnection code because I don't think it really applies.. but if you want it I can post it
The only line that matters is:
(this is called after all the connections are done.)
GetInfoViewController *getinfoVC = [[GetInfoViewController alloc]init];
[getinfoVC endProgressHUD];
I call the endProgressHUD method of the getInfoViewController successfully, as it logs "end called". however, the progress hud stays spinning and does not hide.
Any input would be helpful.
Try this method when showing the HUD.
- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated;
It is a method of MBProgressHUD - selector should be the yelpConnectionMethod and target the serverConnection, object = nil and animated depends on you :)

Unbalanced calls to begin/end appearance transitions for (I believe this is UINavigationController related)

It looks like this topic comes up a lot. I read through several answers but none were the same case as mine so please excuse me if you've seen similar before.
All of my UIViewControllers are being controlled by UINavgationController. On the first UIViewController (SMOnboardingPhotoMarketingViewController), I call into my keychain wrapper class to see if there is anyone logged in (app resuming). If so I call the segue to go to my main logged in screen (SMThumbnailViewController), where I'm getting the error message: Unbalanced calls to begin/end appearance transitions for .
I have examined all of the view controller life-cycle calls to ensure that I'm calling [super method] if I over-rode them. Done.
Other than that this is just a standard push type segue for all transitions. I don't understand what is so different about this call to a segue than all the others that are triggered by button actions. Here is the code from my first view controller:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self loadScrollViewContent];
__weak SMOnboardingPhotoMarketingViewController *weakSelf = self;
[SMAuthentication validateStoredTokenWithCompletion:^(BOOL valid) {
if(valid){
NSLog(#"Logged in. Continue to thumbs page");
[weakSelf performSegueWithIdentifier:kSeguePhotoMarketingToThumbnails sender:self];
}
else{
[SMAuthentication logOut];
NSLog(#"invalid credentials stored. User must log in ");
}
}];
}
I've noticed that in my main view controller (the one that the above code navigates to), viewDidLoad is called, but viewDidAppear is never called. What could cause such an imbalance?
Edit: Adding info. I should state taht if I move the segue call to the outside of that block, the transition goes as normal with no error. Example:
// I know this is ugly. It is for testing only
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self loadScrollViewContent];
__block BOOL complete = NO;
__block BOOL isValid = NO;
[SMAuthentication validateStoredTokenWithCompletion:^(BOOL valid) {
if(valid){
NSLog(#"Logged in. Continue to thumbs page");
isValid = YES;
}
else{
[SMAuthentication logOut];
NSLog(#"invalid tokens stored. User must log in ");
}
complete = YES;
}];
while (!complete) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
if(isValid){
[self performSegueWithIdentifier:kSeguePhotoMarketingToThumbnails sender:self];
}
}
You probably have implemented the following ViewController custom container methods:
- (void)endAppearanceTransition
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated
I once written and forgot them in a base class and they messed up the whole appearance forwarding to child controllers when using storyboards.

Activity Indicator is not showing with SVProgressHUD

I am trying to implement the SVProgressHUD progress activity indicator. I copied the class from the [demo].1
My app loads up but the activity indicator doesn't show up. This is my first time trying to use one of these, so any help would be appreciated.
Here is the code:
#import "SVProgressHUD.h"
#implementation QuotesAppDelegate
- (void)startLoading
{
//call this in your app delegate instead of setting window.rootViewController to your main view controller
//you can show a UIActivityIndiocatorView here or something if you like
[SVProgressHUD show];
[self performSelectorInBackground:#selector(loadInBackground) withObject:nil];
}
- (void)loadInBackground
{
//do your loading here
//this is in the background, so don't try to access any UI elements
[self populateFromDatabase];
[SVProgressHUD dismiss];
[self performSelectorOnMainThread:#selector(finishedLoading) withObject:nil waitUntilDone:NO];
}
- (void)finishedLoading
{
//back on the main thread now, it's safe to show your view controller
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
}
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[self copyDatabaseIfNeeded];
[self startLoading];
}
For anyone else having a similar problem, this can also happen because you have a long loop or a piece of code that takes a long time to execute. If this happens, your progress bar wont be shown until after the loop, which kind of defeats the purpose.
To solve this issue you need to you this:
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
Basically your code would look something like this:
- (IBAction)submitPost:(id)sender {
//now we show the loading bar and submit the comment
[SVProgressHUD showWithStatus:#"Submitting post" maskType:SVProgressHUDMaskTypeGradient];
SEL aSelector = #selector(submitDataOfPost);
[self performSelectorInBackground:aSelector withObject:sender];
}
This will basically load the progress bar, and in a background thread, the method you want to execute will be called. This makes sure that the UI is updated (shows the progress hud) at the same time that your code is executed.
First of all you were not adding your SVProgressHUD to the view.
If your class inherited from UIViewController then [self.view addSubview:]; or if your class is simple UIView then [self addSubView:];
I do not understand your requirement but as far as i can understand through your code that you are showing [SVProgressHUD show]; in your startLoading method and then you are calling loadInBackground method in that method where you are hiding your hud using [SVProgressHUD dismiss];
I will suggest you to trace it by using breakpoint and figure it out.
I had the same problem. When I changed a version of SVProgressHUD to the later one the problem disappeared. My current version supports ARC.
=>
(IBAction)fetchData:(id)sender
{
[SVProgressHUD showWithStatus:#"Loading..." maskType:SVProgressHUDMaskTypeGradient];
[self performSelectorOnMainThread:#selector(getDataFromSomeWhere) withObject:nil waitUntilDone:NO];
}
=>
(void)getDataFromSomeWhere
{
//Do your data populating here. and dismiss the ProgressHud.
[SVProgressHUD dismiss];
}

Refresh UINavigationController?

I have a UINavigationController with two ViewControllers on the stack. At a certain point in the program execution, the second view controller is visible on the screen and at that moment, I would like to replace that ViewController with another. However, it's not working. Here is my code:
UINavigationController * thisNavController = self.waitingController;
// remove the Dummy and set the new page instead
NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
[newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
NSLog (#"visible before: %#", [thisNavController.visibleViewController description]);
[thisNavController setViewControllers: [NSArray arrayWithArray: newControllers] animated: YES];
NSLog (#"visible after: %#", [thisNavController.visibleViewController description]);
[thisNavController.visibleViewController.view setNeedsDisplay];
The above code produces this output:
2011-05-05 13:30:22.201 myApp[3286:207] visible before: <DummyViewController: 0x4c8b4c0>
2011-05-05 13:30:22.209 myApp[3286:207] visible after: <RealViewController: 0x60173f0>
But what is shown on the screen does not change. It seems that everything works fine after I switch tabs, so it seems that it is a redrawing problem, but setNeedsDisplay does nothing and I couldn't find a method that tells the NavigationController that its viewControllers have changed.
Is there some refresh mechanism that I have to trigger to refresh the screen?
One solution would be to say add 2 (initial) view controllers when your app is started, and only allow navigation from the 2nd and 3rd ones, falling back to the 1st (root) view controller in your senario described. You never allow navigation back to this 1st view controller or from this 1st view controller to the 2nd; you see this sort of behaviour in some of Apple's apps, like iTunes and Remote - if there's no network connect the app shows a no-network connection view immediately.
So, when you want to show the 1st view controller above, you do something like:
NSArray *array = [navigationController popToRootViewControllerAnimated:NO];
Without more info about the navigation behaviour of your app I hope this helps.
Or show a modal view controller?
The problem turned out to be the fact that I was trying to replace the view controller stack before the initial transition animation for the Dummy controller has finished. This can be prevented in the following manner.
First, preserve the (eventual) delegate, set the current object as the delegate, set a flag that animation is in progress and push the new controller:
self.oldNavigationControllerDelegate = self.waitingController.navigationController.delegate;
self.waitingController.navigationController.delegate = self;
self.isAnimating = YES;
[viewController.navigationController pushViewController: [[DummyViewController alloc] init] animated: YES];
Then, implement the UIViewControllerDelegate protocol methods as follows:
#pragma mark -
#pragma mark UINavigationControllerDelegate methods
- (void) navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (navigationController == self.waitingController.navigationController)
self.isAnimating = YES;
}
- (void) navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (navigationController == self.waitingController.navigationController) {
self.isAnimating = NO;
if (self.readyPage != nil)
[self pageIsReady: self.readyPage]; // method to load the ready controller
}
}
After that, whenever your content/controller/download/whatever is ready, make sure that the navigation controller is no longer animating. If it is, set a flag that the page is ready. If it isn't, load the page:
if (self.isAnimating)
self.readyPage = controller;
else
[self pageIsReady: controller];
And, of course, implement the actual loading of the new stack (as usual):
- (void) pageIsReady: (UIViewController *) page {
// this method should replace the dummy that is spinning there
UINavigationController * thisNavController = self.waitingController.navigationController;
// remove the Dummy and set the new page instead
NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
[newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
thisNavController.viewControllers = [NSArray arrayWithArray: newControllers];
thisNavController.delegate = self.oldNavigationControllerDelegate; // restore the original delegate
// clean up
self.isAnimating = NO;
self.readyPage = nil;
self.waitingController = nil;
self.oldNavigationControllerDelegate = nil;
}
This makes everybody happy :P