I have a tableView that loads an XML feed as follows:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([stories count] == 0) {
NSString *path = #"http://myurl.com/file.xml";
[self parseXMLFileAtURL:path];
}
}
I'd like to have the spinner to show on the top bar at the app launch and dissapear once the data is displayed on my tableView.
I thought that putting the beginning at viewDidAppear and the end at -(void)parserDidEndDocument:(NSXMLParser *)parser but it didn't work.
I'd appreciate a good explained solution on how to implement this solution.
Here's the problem: NSXMLParser is a synchronous API. That means that as soon as you call parse on your NSXMLParser, that thread is going to be totally stuck parsing xml, which means no UI updates.
Here's how I usually solve this:
- (void) startThingsUp {
//put the spinner onto the screen
//start the spinner animating
NSString *path = #"http://myurl.com/file.xml";
[self performSelectorInBackground:#selector(parseXMLFileAtURL:) withObject:path];
}
- (void) parseXMLFileAtURL:(NSString *)path {
//do stuff
[xmlParser parse];
[self performSelectorOnMainThread:#selector(doneParsing) withObject:nil waitUntilDone:NO];
}
- (void) doneParsing {
//stop the spinner
//remove it from the screen
}
I've used this method many times, and it works beautifully.
Starting a new thread can be overkilling and a source of complexity if you want to do things that are supposed to start on the main thread.
In my own code, I need to start a MailComposer by pushing a button but it can take some time to appear and I want to make sure the UIActivityIndicator is spinning meanwhile.
This is what I do :
-(void)submit_Clicked:(id)event
{
[self.spinner startAnimating];
[self performSelector:#selector(displayComposerSheet) withObject:nil afterDelay:0];
}
It will queue displayComposerSheet instead of executing it straight away. Enough for the spinner to start animating !
I typically implement an NSTimer that will call my spinner method, which I fire off right before I go into doing the heavy work (the work that will typically block the main thread).
The NSTimer fires and my spinner method is called. When the main work is finished, I disable the spinner.
Code for that is like:
IBOutlet UIActiviyIndicatorView *loginIndicator;
{
...
[loginIndicator startAnimating];
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(executeAuthenticationRequest)
userInfo:nil repeats:NO];
...
}
- (void) executeAuthenticationRequest
{
/* Simulate doing a network call. */
sleep(3);
[loginIndicator stopAnimating];
...
}
You can also do:
IBOutlet NSProgressIndicator *pIndicator;
Start:
[pIndicator startAnimation:self];
[pIndicator setHidden:NO];
And Stop:
[pIndicator stopAnimation:self];
[pIndicator setHidden:YES];
In Cocoa (and most other app frameworks) the user interface is updated by the main thread. When you manipulate views, they are typically not redrawn until control returns to the run loop and the screen is updated.
Because you are parsing the XML in the main thread, you are not allowing the screen to update, and that is why your activity indicator is not appearing.
You should be able to fix it by doing the following:
In viewDidAppear, show/animate the spinner and then call
[self performSelector:#selector(myXMLParsingMethod) withObject:nil afterDelay:0];
In myXMLParsingMethod, parse your XML, then hide/stop the spinner.
This way, control will return to the run loop before parsing begins, to allow the spinner to begin animating.
Related
I am using the [performSelector:#selector(reloadData) withObject:nil afterDelay:0.01] inside IBAction of a UIButton, the reloadData method draw some subviews on the main view in a particular way, the issue is when I tap the button quickly and repeatedly the selector "ReloadData" executed multiple times, event though I am canceling the all previous requests to that selector, and this results in duplication for the subviews in the main view
-(IBAction) myButtonIsTapped
{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(reloadData) object:nil];
[self performSelector:#selector(reloadData) withObject:nil afterDelay:0.01];
}
and reload data method like the following:
-(void) reloadData
{
#synchronized(self){
// clear all subviews from the main view
// draw new subviews
}
}
What about this:
-(IBAction) myButtonIsTapped
{
[self.myButton setUserInteractionEnabled:NO];
[self performSelector:#selector(reloadData) withObject:nil afterDelay:0.01];
}
-(void) reloadData
{
// Long task...
// Enable the button again:
[self.myButton setUserInteractionEnabled:YES];
}
Sometimes is just easier to control what the user is doing (UI), than logically dealing with what he has done.
Do one Thing create one BOOL variable and set in viewdidload yes and check in function if it is yes then method call and also make it no in buttonmake method.
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];
}
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...
Consider this:
#interface SomeViewController : UIViewController {
SomeChildObject *child;
}
#end
#implementation SomeViewController
- (void) viewDidLoad {
...
child.delegate = self;
}
- (void) somethingHappened {
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:child
selector:#selector(doSomething)
object:nil];
[someNsOperationQueue addOperation:operation];
[operation release];
}
- (void) callbackA:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackAonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackAonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
- (void) callbackB:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackBonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackBonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
#end
In English:
I have a view controller running in the main thread with a child model object to do something (fetch data over network). The view controller is the delegate for the child, so the child can signal the results back with delegation. To perform the expensive work, I spawn the child.doSomething method with a NSInvocationOperation that launches the operation in a background thread. When done, the child calls the delegate's (view controller's) callbackA or callbackB with some results. Since (I think) these callbacks are invoked in the same background thread where the doSomething call was run, I need to call performSelectorOnMainThread to transfer control back to main thread.
This works fine, but I do not like having two callback-related methods for each callback. (There are actually more callbacks, so the real code is even more bloated.) Ideally, I would do something like this:
- (void) callbackA:(SomeData *)someData {
if (not_running_on_main_thread) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
} else {
// now running on main thread, work with the results.
}
}
Questions:
1) how do I do the "not_running_on_main_thread" test?
2) is there any other way to cut down the callbacks bloat?
EDIT: ok, I never read NSThread docs before posting :) looks like [NSThread isMainThread] is what I am looking for. But is there any other way to restructure or make this nicer still?
Just check for [NSThread isMainThread]. There's nothing more you can do if you need multiple callbacks which do different things.
Only one thing I do differently, my code looks like this:
- (void) callbackA:(SomeData *)someData {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
return;
}
// now running on main thread, work with the results.
}
This lets me get rid of the whole-function-long else and make the code a little clearer. And you can save on one indentation level this way ;-)
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?