so I have a table view and whenever user press a row, another class view shows up. So I wanted to have a loading indicator in between the transition. I am using MBProgressHUD, but it showed nothing when I pressed the row. And what should I put inside the #selector()?
[loading showWhileExecuting:#selector() onTarget:self withObject:[NSNumber numberWithInt:i] animated:YES];
Here is my code.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
loading = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:loading];
loading.delegate = self;
loading.labelText = #"Loading Events, Please Wait..";
[loading showWhileExecuting:#selector(//what should I put) onTarget:self withObject:nil animated:YES];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if ([[self.citiesArray objectAtIndex:indexPath.row] isEqual:#"NEW YORK"])
{
self.newYorkViewController = [[NewYorkViewController alloc] initWithNibName:#"NewYorkViewController" bundle:nil];
Twangoo_AppAppDelegate *delegate = (Twangoo_AppAppDelegate*)[[UIApplication sharedApplication] delegate];
[delegate.citiesNavController pushViewController:self.newYorkViewController animated:YES];
}
}
You need to implement a waiting function so that when the control returns from that method the HUD will hide/disappear from the screen. So its basically what you do when the HUD is being displayed on the screen, it may be some processing or wait for response for a http request etc. It can also be a timer.
- (void)waitForResponse
{
while (/*Some condition is not met*/)
{
}
}
Also you need to implement the
- (void) hudWasHidden
{
[HUD removeFromSuperview];
[HUD release];
}
You might have a look to the Cocoa documentation chapter on selectors
Selector can be simply see as a pointer to a function.
Then, I guess you are trying to display a progress hud while a particular process is running .. this particular process logically should be isolated in a dedicated method (let's call it doTheJob ).
So start by creating a dedicated method named whatever ( here doTheJob )
- (void) doTheJob;
That being said the MBProgressHUD allows you to simply specify the working method that should be handled by the progress information using the showWhileExecuting method. And the selector is here to defined the target worker method.
[loading showWhileExecuting:#selector(doTheJob) onTarget:self withObject:nil animated:YES];
The target would be the object reference that defines the selector. To remain simple, if you define the method doTheJob in the current class use self as target.
and the withObject, is any parameter that you want / need to provide to the selector method. Beware that if you need to provide parameter to the target method, you need to extend the selector definition with an trailing colon as #selector(doTheJob:)
Hope this helps.
Related
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.
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 :)
So when I click on a callout accessory in my mapView, nothing happens for several seconds because it is making a url request and parsing it, so I wanted to show the activity indicator so the user doesn't think it's frozen. Here's the code:
- (void)mapView:(MKMapView *)mv annotationView:(MKAnnotationView *)pin calloutAccessoryControlTapped:(UIControl *)control {
// start activity indicator
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSLog(#"tapped");
ArtPiece *artPiece = (ArtPiece *)pin.annotation;
//when annotation is tapped switches page to the art description page
artDescription *artD = [[artDescription alloc] initWithNibName:#"artDescription" bundle:nil];
artD.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
artD.startingLocation = mapView.userLocation.location.coordinate;
artD.selectedArtPiece = artPiece;
NSLog(#"0");
[self presentModalViewController:artD animated:YES];
NSLog(#"1");
[artD loadArt:artPiece];
NSLog(#"2");
// stop activity indicator
//[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[artD release];
}
Strangely (to me anyway, maybe I'm missing something obvious as I'm pretty inexperienced), the activity indicator does not show until after the method is done, and the modal view starts animating into view. I put the NSLogs in to see what was taking time. I had about a 2 second pause between "0" and "1" and another couple seconds between "1" and "2". Then the indicator finally showed, so I am sure it is waiting until the end of the method for some reason. Any ideas why?
The change to the UI, displaying the activity indicator, does not take effect until control has returned to the application's main run loop. This does not occur until after your method has ended and the stack has unwound. You need to show the activity indicator, then dump the activity you are waiting for onto a background thread:
[self performSelectorInBackground:#selector(doThingINeedToWaitFor:)
withObject:anObject];
(Note that Apple recommends that you move away from using threads explicitly; performSelectorInBackground:withObject: is the simplest method to get some code run off the main thread. More complex options are available for other situations. See the Concurrency Programming Guide.)
The important gotcha is that UI updates still need to be handled on the main thread, so in that method, when the work is done, you need to call back to stop the activity indicator:
- (void) doThingINeedToWaitFor: (id)anObject {
// Creating an autorelease pool should be the first thing
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Do your work...
// ...
// Update the UI back on the main thread
[self performSelectorOnMainThread:#selector(allDoneWaiting:)
withObject:nil
waitUntilDone:YES];
// Clear out the pool as the final action on the thread
[pool drain];
}
In your callback method, you hide the activity indicator again and do any other post-processing that's necessary.
You cannot start and stop the activity indicator in the same function.
See the answer I provided for this question: How show activity-indicator when press button for upload next view or webview?
Edit for clarity:
- (void) someFunction
{
[activityIndicator startAnimation];
// do computations ....
[activityIndicator stopAnimation];
}
The above code will not work because you do not give the UI time to update when you include the activityIndicator in your currently running function. So what I and many others do is break it up into a separate thread like so:
- (void) yourMainFunction {
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[NSThread detachNewThreadSelector:#selector(threadStartAnimating) toTarget:self withObject:nil];
//Your computations
[activityIndicator stopAnimating];
}
- (void) threadStartAnimating {
[activityIndicator startAnimating];
}
Something is slowing down your spinner. I would recommend doing your heavy lifting in background, using a thread. Try this:
-(void)myMethod{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[NSThread detachNewThreadSelector:#selector(startWorkingThread) toTarget:self withObject:nil];
}
-(void)startWorkingThread{
//Heavy lifting
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
I assume that you have commented the:
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
For testing purposes...
I have a strange problem with a UIView :
I want to show an Activity Indicator View that I created with Interface Builder to indicate long running activity.
In the viewDidLoad function of my principal viewController I init the ActivityIndicator View like this :
- (void)viewDidLoad {
[super viewDidLoad];
appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
load = [[ActivityIndicatorViewController alloc] init];
...
When I push a button it call this IBAction :
- (IBAction)LaunchButtonPressed{
// Show the Activity indicator view.
[self.view addSubview:load.view];
// eavy work
[self StartWorking];
// Hide the loading view.
[load.view removeFromSuperview];
}
In the StartWorking function, I ask a request to an internet serveur and parse the XML file that it return me.
The problem is that if I Call my StartWorking function, the application do not start by showing the Activity Indicator view but with the StartWorking function.
Whereas if I remove the call to StartWorking function, the view is shown.
Is someone able to explain me why? :s
Have you tried to call the StartWorking method on a different thread?
Maybe its heavy process prevents other instructions to take place.
Look at the NSThread class, especially the detachNewThreadSelector:toTarget:withObject: method.
EDIT: About the pool problem, you need to create a pool in your StartWorking method, if it's called on a different thread:
- ( void )StartWorking
{
NSAutoreleasePool * pool = [ [ NSAutoreleasePool alloc ] init ];
/* Code here... */
[ pool release ];
}
Replace :
[self.view addSubview:load.view];
With :
[self performSelector:#selector(addLoadingSubview) afterDelay:0.1f];
And create the method :
-(void)addLoadingSubview{[self.view addSubview:load.view];}
Ok, I found a solution based on santoni answer :
- (IBAction)LaunchButtonPressed{
// Show the Activity indicator view.
[self performSelector:#selector(ShowActivityIndicatorView) withObject:nil afterDelay:0];
// eavy work
[self performSelector:#selector(StartWorking) withObject:nil afterDelay:2];
// Hide the loading view.
[load.view removeFromSuperview];
}
The Activity Indicator view is dislayed before the call to the eavy function.
Thank's for answering.
I have an application that has a UITableView. This UITableView is populated by an NSMutableArray being held (as a property) in the appDelegate. You can think of this as an email window. It lists messages in a subclassed UITableViewCell. When a new message appears, I have all the code done which downloads the message, adds the data to the appDelegate's NSMutableArray which holds all of the messages. This code is working fine.
Now, once the new message is downloaded and added to the array, I am trying to update my UITableView using the following code, however - the UITableView's delegate functions do not get called.
The odd thing is when I scroll my UITableView up and down, the delegate methods finally get called and my section headers DO change (they show the message count for that section). Shoudn't they update in real-time and not wait for my scrolling to trigger the refresh? Also, the new cell is never added in the section!!
Please Help!!
APPDELEGATE CODE:
[self refreshMessagesDisplay]; //This is a call placed in the msg download method
-(void)refreshMessagesDisplay{
[self performSelectorOnMainThread:#selector(performMessageDisplay) withObject:nil waitUntilDone:NO];
}
-(void)performMessageDisplay{
[myMessagesView refresh];
}
UITableViewController Code:
-(void) refresh{
iPhone_PNPAppDelegate *mainDelegate = (iPhone_PNPAppDelegate *)[[UIApplication sharedApplication] delegate];
//self.messages is copied from appDelegate to get (old and) new messages.
self.messages=mainDelegate.messages;
//Some array manipulation takes place here.
[theTable reloadData];
[theTable setNeedsLayout]; //added out of desperation
[theTable setNeedsDisplay]; //added out of desperation
}
As a sanity check, have you verified that theTable is not nil at that point?
You could try putting a delay on the reloadData call - I had a similar problem when I was trying to get my tableview to update when reordering cells, except that the app crashed if I called reloadData during it.
So something like this might be worth a try:
Refresh method:
- (void)refreshDisplay:(UITableView *)tableView {
[tableView reloadData];
}
and then call it with (say) a 0.5 second delay:
[self performSelector:(#selector(refreshDisplay:)) withObject:(tableView) afterDelay:0.5];
Hope it works...
If you call reloadData from within a dispatched method, make sure to execute it on the main queue.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^(void) {
// hard work/updating here
// when finished ...
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self.myTableView reloadData];
});
});
..same in method form:
-(void)updateDataInBackground {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^(void) {
// hard work/updating here
// when finished ...
[self reloadTable];
});
}
-(void)reloadTable {
dispatch_async(dispatch_get_main_queue(), ^(void) {
[myTableView reloadData];
});
}
Have you tried setting a breakpoint in your refresh method just to be sure your messages array has the correct content before calling reloadData?