I want to show an MBProgressHUD in my iPhone app without spawning new threads.
I have a very complicated set of business logic which sometimes (but not always) needs to wait for user input, and running on multiple threads ends up asking for user input multiple times at once, leading to crazy errors. Thus I would prefer to avoid running anything off of the main thread. However, due to this constraint, MBProgressHUD is not showing because the main thread is being blocked! Normally I would create my MBProgressHUD with the following code:
[HUD showWhileExecuting:#selector(myWorkerMethod) onTarget:self withObject:nil animated:YES];
But I would like to use the following code without blocking the main thread:
HUD = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:HUD];
HUD.delegate = self;
HUD.minShowTime = 0.0;
HUD.labelText = #"some text";
[HUD show:YES];
Any thoughts?
How about this?
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
HUD.labelText = #"Foo";
// code to execute synchronously
[MBProgressHUD hideHUDForView:self.view animated:YES];
Better late than never. You can do this with some run loop tickery. See this answer for details.
Related
First time working with a HUD and I'm confused.
I setup the HUD like this in my viewDidLoad:
[MBProgressHUD showHUDAddedTo:self.view animated:YES];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[[[WSXmppUserManager shared] xmppStreamManager] connect];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
});
});
The HUD doesn't show. I think the reason is as follows. The xmpp connect method fires off a connection request to the xmpp server and then it's done. So there is no activity to wait for as is.
However, the connection isn't established until the server responds and the following delegate method is fired:
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
I want to wait for this and only then dismiss the HUD, but I'm at a loss as to how to do that. I'm missing something very simple.
You need to move this code
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
});
After your long running method has finished... that is, if this code is indeed returning immediately
[[WSXmppUserManager shared] xmppStreamManager] connect];
The hud is likely never going to display... as it gets told to display and then told to hide on the same run loop or perhaps one run loop right afterwards...
Why not put it at the end of this method if this indicates that a response has been received and the operation is completed?
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
HUD =[[MBProgressHUD alloc] initWithWindow:self.view];
[HUD setDelegate:self];
[self.view addSubview:HUD];
[HUD showWhileExecuting:#selector(connectToServer)
onTarget:self
withObject:nil
animated:YES];
In the connectToServer
-(void)connectToServer
{
[[[WSXmppUserManager shared] xmppStreamManager] connect];
}
As soon as the connectToServer method comepletes it task in the background , a delegate of MBProgressHUD called hudWasHidden: is automatically called
-(void)hudWasHidden:(MBProgressHUD *)hud
{
//Further work after the background task completed
}
I have a method to get contacts from the address book and doing some stuff with them ("getContactInformation").
This process is a bit long (a few seconds) and after this process I show a new ViewController. To make it friendly to the user I would like to use MBProgressHUD to show an activity indicator at the beginning of the process and hide it at the end.
Which is the best way to do it? I've test this:
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = #"Retrieving information...";
[self getContactInformation];
[MBProgressHUD hideHUDForView:self.view animated:YES];
[self presentViewController:newController animated:NO completion:nil];
But it doesn't work (It doesn't show the indicator). Anyone can help me?
Thank you in advance
Keep a separate method for [self presentViewController:newController animated:NO completion:nil];. And try calling that method after some particular delay. That should be solving the problem.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[self getContactInformation];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.mapView animated:YES];
});
});
This will spawn a thread to run getContactInformation and won't block the main thread which is required to do UI changes (such as displaying the HUD).
I'm guessing the main thread is getting locked up when you get contact info and doesn't have time to update to show the HUD. Once the method is complete, it gets the main thread again to update the UI (removing the HUD).
In the viewDidload method, i am using MBProgressHUD to show an activity indicator until data is downloaded from the net, and thereafter to check if the application has enabled GPS. based on this i alert the user using MBProgressHUD.
My code;
ViewDidLoad
hudForDownloadData = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view hudForDownloadData ];
hudForDownloadData .delegate = self;
hudForDownloadData .labelText = #"Loading";
hudForDownloadData .detailsLabelText = #"updating data";
[hudForDownloadData showWhileExecuting:#selector(downloadDataFromWebService) onTarget:self withObject:nil animated:YES];
[self downloadDataFromWebService]; // This method is called, and then data is downloaded from the webservice, once the data is downloaded i will remove the `MBProgressHUD ` alert
Now i am checking if the application has enabled access to GPS, and alert the user if not.
hudForGPS = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view addSubview:HUD];
hudForGPS .delegate = self;
hudForGPS .labelText = #"Loading";
hudForGPS .detailsLabelText = #"updating data";
[hudForGPS showWhileExecuting:#selector(checkIfApplicationEnabledGPS)
onTarget:self withObject:nil animated:YES]; // This will only take a
maximum of 2-3 seconds. and i will remove it after that
The problem is that; when the internet/wifi is down, the hudForDownloadData will continuously animate (display uiactivityindicator). In the meantime hudForGPS will also execute. But it will display below hudForDownloadData . So the user will not be able to see it.
What i want to do is to execute the checkIfApplicationEnabledGPS first, and wait till its alert finishes (it will only take 2-3 seconds), and then load execute the downloadDataFromWebService where if the internet/wifi is not available it will display forever.
How can i do this programatically ?
Execute checkIfApplicationEnabledGPS first and when it is finished,you show an alert box,If you are using UiAlertView to show the alert then you can handle it's delegate:-
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
when the user presses ok button in UiAlert view you can override the above delegate with hudForDownloadData
In this way both the checkIfApplicationEnabledGPS and hudForDownloadData will be executed one after the other
in a method, i want to call a method after n seconds:
self.toolBarState = [NSNumber numberWithInt:1];
[self changeButtonNames];
[self drawMap];
[self performSelector:#selector(showActionSheet) withObject:nil afterDelay:2];
i want to show the action sheet 2 seconds after drawMap is complete. when i use this performSelector, it never makes the call.
if i just put [self showActionSheet];, it works just fine. is there reason why the performSelector is not making the call?
EDIT: in another part of my code, i make the same call and it works:
HUD = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:HUD];
HUD.delegate = (id) self;
[HUD showWhileExecuting:#selector(drawMap) onTarget:self withObject:nil animated:YES];
[self performSelector:#selector(showActionSheet) withObject:nil afterDelay:6];
here, the showActionSheet gets called 6 seconds after drawMap has completed. i'm guessing there is something going on with the threads that i don't understand yet...
EDIT2:
-(void)showActionSheet
{
InspectAppDelegate *dataCenter = (InspectAppDelegate *) [[UIApplication sharedApplication] delegate];
if (dataCenter.fieldIDToPass == nil)
{
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:#"Selected Boundary Options" delegate:(id) self cancelButtonTitle:#"Cancel" destructiveButtonTitle:nil otherButtonTitles:#"Analyze a Field",#"Retrieve Saved Analysi", #"Geotag Photos", #"Refresh the map",nil];
actionSheet.tag = 0;
[actionSheet showInView:self.view];
}
else
{
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:#"Selected Boundary Options" delegate:(id) self cancelButtonTitle:#"Cancel" destructiveButtonTitle:nil otherButtonTitles:#"Analyze a Field",#"Retrieve Saved Analysi", #"Geotag Photos", #"Attribute the Field", #"Refresh the map",nil];
actionSheet.tag = 0;
[actionSheet showInView:self.view];
}
}
EDIT3:
ok, so the progress of method calls is:
-(void) foundDoubleTap:(UITapGestureRecognizer *) recognizer
{
[HUD showWhileExecuting:#selector(select) onTarget:self withObject:nil animated:YES];
}
-(void) select
{
[self changeButtonNames];
[self drawMap];
[self performSelector:#selector(showActionSheet) withObject:nil afterDelay:2];
}
showActionSheet never gets called. like i said, i'm pretty sure its a threading issue. if call it with [self showActionSheet], it will run. =/
I ran into this same issue, and by necessity I solve it slightly different from the accepted answer. Notice I wanted my delay and selectors to be variables? Using a block allows me to stay within my current method.
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:loopSelector withObject:nil afterDelay:cycleTime];
});
By the way, this is definitely a threading issue. The documentation for performSelector:withObject:afterDelay: states that this will be performed on the current thread after the delay, but sometimes that thread's run loop is no longer active.
A more detailed discussion on the subject can be found here
Try using:
-(void) select {
[self changeButtonNames];
[self drawMap];
[self performSelectorOnMainThread:#selector(showActionSheet) withObject:nil waitUntilDone:YES];
}
-performSelector:withObject:afterDelay: schedules a timer on the same thread to call the selector after the passed delay.
Maybe this will work for you:
-(void) select {
[self changeButtonNames];
[self drawMap];
[self performSelectorOnMainThread:#selector(someA) withObject:nil waitUntilDone:YES];
}
-(void)someA {
[self performSelector:#selector(showActionSheet) withObject:nil afterDelay:2];
}
Does your class still exist in memory?
If your class goes away before the performSelector fires, do you wind up sending the message to nil (which would cause nothing to occur).
You could test this by dropping an NSLog into your dealloc()
You mentioned threads. If your performSelector isn't called from the MainThread it could cause issues (UI things should be done on the main thread).
I got the same problem when I call performSelector:withObject:afterDelay: in a background thread created by ReactiveCocoa.
If I execute the block in the ReactiveCocoa's way, the block will be called correctly:
[[[RACSignal empty] delay:2.0] subscribeCompleted:^(){
// do something
}];
I guess there is some magic in the ReactiveCocoa's threading model.
I've got both working great individually, but when I try to combine them like this:
- (IBAction)showWithLabel:(id)sender
{
HUD = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
[self.checkinsViewController.view addSubview:HUD];
HUD.delegate = self;
HUD.labelText = #"Sending tweet";
[HUD showWhileExecuting:#selector(tweet) onTarget:self withObject:nil animated:YES];
}
- (void)tweet { [_twEngine sendUpdate:#"Test tweet"]; }
I don't get any errors, but the tweet isn't sent If I place:
[_twEngine sendUpdate:#"Test tweet"];
In the IBAction, it tweets. If I change tweet to sleep, the HUD shows up properly.
Any ideas?
The showHUDAddedTo:animated: and showWhileExecuting: methods are mutually exclusive. You can't use both methods to show the HUD .
Change your initializer to just allocate a HUD and it should work.
HUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];