I have a button on the currently navigated to viewcontroller, connected to an IBAction.
In the IBAction I create a UIActivityIndicatorView as usual, with [self.view addSubView], then load some pictures.
I've tried setNeedsDisplay on the indicator view, the view controller, and the window, but it still loads the pictures before showing the indicator, which of course is quite useless to me.
So I'm looking for a way to either force an instant redraw (which when I think a little more about it is unlikely to make work), or a way to load the pictures after the indicator has appeared, or a way to launch a separate thread or similar to start animating / show the indicator, or put the indicator in a separate viewcontroller and somehow force it to add/show itself before going on to the picture-loading.
Recommendations?
What I do in this situation is spawn a new thread, which frees up the main thread to handle UI interaction while stuff is loading in the background.
First show the UIActivityIndicatorView, then spawn a new thread that loads the images, then on the last line of the method that is executed in the new thread, hide the UIActivityIndicatorView.
Here's an example:
//do stuff...
[activityIndicatorView startAnimating];
[NSThread detachNewThreadSelector:#selector(loadImages) toTarget:self withObject:nil];
In your loadImages method:
- (void) loadImages {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//load images...
[activityIndicatorView performSelectorOnMainThread:#selector(stopAnimating)];
[pool drain];
}
Related
I have two methods on my app delegate to start and stop an activity indicator. I need to call them on a background thread so that it is always visible. Like this:
[self.delegate performSelectorInBackground:#selector(acionarActivityIndicator) withObject:nil];
or
[NSThread detachNewThreadSelector:#selector(acionarActivityIndicator) toTarget:self.delegate withObject:nil];
Both work the same.
Suppose this is VCA button click, then, on VCB after loading everything, I call another method in my app delegate to stop animating. I perform other actions in the background on both views to get my data.
The activity indicator works fine. The problem is that after a while, all animations in my app stop working. I feel like it is related to this, but I'm not sure.
If I try to make screen transitions faster, I get this bug earlier. Does that make sense?
App Delegate methods:
- (void)acionarActivityIndicator {
// Show the activity indicator
self.view = [[UIView alloc] init];
self.activityIndicator = [[UIActivityIndicatorView alloc] init];
self.view.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
self.view.frame = self.window.bounds;
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
CGRect frame = self.view.frame;
self.activityIndicator.center = CGPointMake(frame.size.width/2, frame.size.height/2);
[self.view addSubview:self.activityIndicator];
[self.activityIndicator startAnimating];
[self.window addSubview:self.view];
[self.window makeKeyAndVisible];
}
- (void)pararActivityIndicator
{
// metodo parar activity indicator
[self.view removeFromSuperview];
[self.activityIndicator stopAnimating];
}
I would say that your problem is that you are manipulating your UI on a background thread. The documentation clearly says that you shouldn't do so:
Note: For the most part, UIKit classes should be used only from an application’s main thread. This is particularly true for classes derived from UIResponder or that involve manipulating your application’s user interface in any way.
UIActivityIndicatorView, which you are using, inherits from UIResponder so what you are doing falls under the "particularly true" case.
You are probably doing long synchronous work on the main thread which is what split the work into multiple threads in the beginning. For this I have 2 things to say:
1. You divided the work in the wrong way
It sound like you kept the work / data processing / networking / other long running synchronous task on the main thread and chose to dispatch UI updates to another thread. As the documentation (quoted above) says, this is the wrong order. You should do any long running synchronous task in the background and call back to the main thread when the UI needs to update.
2. There are technologies that are much easier to use than NSThread
You chose to use NSThread or other thread related technologies to divide your work. While this is the case on other platforms. iOS prefers to use queues for concurrency.
At a lower level you have grand central dispatch (GCD for short) that allows you to queue up a block on the main queue or on a background queue. You also have NSOperations to divide the work into operations that can have dependencies in between each other.
For what you are doing, I would try something along these lines:
- (IBAction)doHeavyWork:(id)sender
{
/* start the activity indicator (you are now on the main queue) */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/* Do some heavy work (you are now on a background queue) */
dispatch_sync(dispatch_get_main_queue(), ^{
/* stop the activity indicator (you are now on the main queue again) */
});
});
}
And also, take another look at the UIKit documentation and perhaps the iOS App Programming Guide.
I'm using the MBProgressHUD library in my app, but there are times that the progress hud doesn't even show when i query extensive amount of data, or show right after the processing of data is finished (by that time i don't need the hud to be displayed anymore).
In another post i found out that sometimes UI run cycles are so busy that they don't get to refresh completely, so i used a solution that partially solved my problem: Now every request rises the HUD but pretty much half the times the app crashes. Why? That's where I need some help.
I have a table view, in the delegate method didSelectRowAtIndexPath i have this code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[NSThread detachNewThreadSelector:#selector(showHUD) toTarget:self withObject:nil];
...
}
Then, I have this method:
- (void)showHUD {
#autoreleasepool {
[HUD show:YES];
}
}
At some other point I just call:
[HUD hide:YES];
And well, when it works it works, hud shows, stays and then disappear as expected, and sometimes it just crashes the application. The error: EXC_BAD_ACCESS . Why?
By the way, the HUD object is already allocated in the viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
...
// Allocating HUD
HUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view addSubview:HUD];
HUD.labelText = #"Checking";
HUD.detailsLabelText = #"Products";
HUD.dimBackground = YES;
}
You need to perform your processing on another thread, otherwise the processing is blocking MBProgressHud drawing until it completes, at which point MBProgressHud is hidden again.
NSThread is a bit too low-level for just offloading processing. I'd suggest either Grand Central Dispatch or NSOperationQueue.
http://jeffreysambells.com/2013/03/01/asynchronous-operations-in-ios-with-grand-central-dispatch
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
/* Prepare the UI before the processing starts (i.e. show MBProgressHud) */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/* Processing here */
dispatch_async(dispatch_get_main_queue(), ^{
/* Update the UI here (i.e. hide MBProgressHud, etc..) */
});
});
This snippet will let you do any UI work on the main thread, before dispatching the processing to another thread. It then returns to the main thread once the processing is done, to allow you to update the UI.
I loaded a UIView from a UIViewController. This UIView contains a (big) UICollectionView.
The transition from the first UIView to the second UIView is very slow: It seems that when the rendering of all collection's cells is done the second view can show up.
In the second UIView, I tried.
- (void)viewDidAppear:(BOOL)animated
{
[activityView stopAnimating];
NSLog(#"did appear %#",[NSDate date]);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[activityView startAnimating];
NSLog(#"will appear %#",[NSDate date]);
}
In the NSLog, there is no time difference between the two events, and in fact the second UIView shows up in about 1 second after the event viewDidAppear.
At this point, I would start a UIActivityIndicator, as in the code. But the indicator is never shown.
Any hint?
Your problem here is that you're probably blocking the main thread by maybe doing some disk IO or network activity or heavy computations, and that is why you're experiencing this delay.
I'd recommend that you do all this on a secondary thread while showing a UIActivityIndicator. On the completion you can then hide the activity indicator and show the collection view.
EDIT:
N.B. There is probably a better way to go, but i'm not very familiar with collection views.
A really easy fix would be to keep a BOOL ivar in the view controller where you load the collection view. Call it shouldLoadData and set it to NO in your viewDidLoad method. Then all you need to do is to return 0 to your UICollectionViewDelegate methods numberOfSectionsInCollectionView: and collectionView:numberOfItemsInSection:.
Finally in your viewDidAppear method, you set shouldLoadData to YES and call reloadData on your collectionView. The tricky part at this point is to figure out a way to tell when the collection view finished reloading its data so that you can stop the activity indicator.
I found out that it is not even that tricky, reloadData just queues up on the main thread, so you can just queue another task on the main thread after you make the call to reloadData. Just do:
[self.collectionView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.activity stopAnimating];
});
And you'll get the desired behaviour. You should be aware, however, that this would still block the main thread.
E.g. if you have a back button, it could not be pressed until the data is fully loaded (it could actually be pressed, but it would not have any visible effect until then).
I have an PlayPageViewController as rootViewController. The PlayPageViewController will display 3D models and UIImages in an method call editPages(), which will takes several seconds to wait.
I just want to add an loadingView at start and when PlayPageViewController gets fully loaded it will disappear.
Here is my solution:
Add an loadingView with activityIndicator.
When the loadingView is loaded, I will begin to implement
but seems it didn't work
STLoadingViewController *loadingView =
[[STLoadingViewController alloc]initWithNibName:#"STLoadingViewController"
bundle:nil];
loadingView.view.frame=CGRectMake(0, 0, 1024, 768);
[self.view insertSubview:loadingView.view atIndex:3];
if(loadingView.isViewLoaded && loadingView.view.window)
{
[self.view insertSubview:self.playPageViewController.view atIndex:4];
[self.playPageViewController setEditingPage:pageIndex];
[loadingView.view removeFromSuperview];
}
You have to do your respective methods to call in viewDidAppear method when this method is called all the appearing task had been finished.
What is the ViewLoad() method? Do you mean viewDidLoad:? You could setup all your views in the storyboard, including a view containing the activity indicator. Don't load the model at this point but wait until viewDidLoad: is called. At this point you may use Grand Central Dispatch to start the loading of the model. It could look a bit like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// You need to setup the activity indicator outlet in the storyboard
[_activityIndicator startAnimating];
}
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// Do the expensive work on the background
[NSThread sleepForTimeInterval:5.0f];
// All UI related operations must be performed on the main thread!
dispatch_async(dispatch_get_main_queue(), ^{\
// Replace the view containing the activity indicator with the view of your model.
[_activityIndicator stopAnimating];
NSLog(#"STOP");
});
});
}
Edit: The link seems to be down. This is probably due to the current problems with the Dev Center. You can find the documentation in the documentation pane of the Xcode Organizer.
I have a PhotoViewController class with an #property UIActivityIndicatorView* spinner. FlickrPhotoViewController is a subclass of PhotoViewController that downloads a photo from Flickr and tells the spinner when to start and stop animating. 'updatePhoto' is called every time the view controller is given a Flickr photo:
- (void)updatePhoto { // Download photo and set it
NSLog("updatePhoto called");
if (self.spinner) NSLog(#"Spinner exists in updatePhoto");
dispatch_queue_t downloadQueue = dispatch_queue_create("downloader",
NULL);
[self.spinner startAnimating];
dispatch_async(downloadQueue, ^{
// Download the photo
dispatch_async(dispatch_get_main_queue(), ^{
[self.spinner stopAnimating];
// Set the photo in the UI
}
});
});
}
The above methodology is exactly what I use for displaying a spinning wheel in my table view controllers while the table contents download, and it always works there.
You will notice at the beginning of updatePhoto I print a message if the UIActivityIndicatorView exists. I put a similar statement in awakeFromNib, viewDidLoad, and viewWillAppear. When I run it, this is the exact output I get:
2013-01-31 21:30:55.211 FlickrExplorer[1878:c07] updatePhoto called
2013-01-31 21:30:55.222 FlickrExplorer[1878:c07] Spinner exists in viewDidLoad
2013-01-31 21:30:55.223 FlickrExplorer[1878:c07] Spinner exists in viewWillAppear
Why does spinner not exist in awakeFromNib? Docs indicate that "When an object receives an awakeFromNib message, it is guaranteed to have all its outlet and action connections already established." Can an IBOutlet be connected without the existence of the object it is connecting to? In this case, can the spinner IBOutlet be connected to the storyboard without spinner being allocated?
Moving beyond this, I overrode the getter for spinner so that it would instantiate if it does not exist. As a result, the printing output now looks like this:
2013-01-31 21:48:45.646 FlickrExplorer[2222:c07] Spinner exists in awakeFromNib
2013-01-31 21:48:45.647 FlickrExplorer[2222:c07] updatePhoto called
2013-01-31 21:48:45.647 FlickrExplorer[2222:c07] Spinner exists in updatePhoto
2013-01-31 21:48:45.649 FlickrExplorer[2222:c07] Spinner exists in viewDidLoad
2013-01-31 21:48:45.650 FlickrExplorer[2222:c07] Spinner exists in viewWillAppear
This is what I would have expected to see earlier. Nevertheless, I still do not get any animation.
Possible problems that I have ruled out:
The spinner is the same color as the background, making it invisible. In my project, the background is black and the spinner is white.
I am attempting to animate the UIActivityIndicatorView while some expensive method is blocking the main thread. In my project, all my file system I/O and downloading methods are called in a non-main dispatch_async queue.
The spinner's IBOutlet is not hooked up. In my project, I have double checked this numerous times. It can be seen from both the storyboard and the PhotoViewController.h file that it is connected.
All three of these possibilities are ruled out by the fact that putting [self.spinner startAnimating]; in viewWillAppear makes it successfully animate throughout the download process.
You can download this project if you like. Just go to any screen that attempts to display a large photo and you will see that the spinner does not appear. There are many problems with this project, but this is the one I am focusing on now.
Edit 1:
I added the project's missing dependencies on Git, so the project will
now compile for you
Edit 2 (2 February 2013):
I am seeing this problem only on the iPhone when the updatePhoto method is called due to another view controller's prepareForSegue setting the photo in the FlickrPhotoViewController. Is it possible that this contributes to the problem?
Try to fire start animating uiactivityindictor before u call updatePoto method like this
Tru calling this iinstead of calling UpdatePhoto just all :
[self startAnimatingAndThenUpdatePhoto];
-(void)startAnimatingAndThenUpdatePhoto{
if (self.spinner) NSLog(#"Spinner exists in updatePhoto");
[self.spinner startAnimating];
[self performSelector:#selector(updatePhoto) withObject:nil afterDelay:0.01];
}
- (void)updatePhoto { // Download photo and set it
NSLog("updatePhoto called");
dispatch_queue_t downloadQueue = dispatch_queue_create("downloader",
NULL);
dispatch_async(downloadQueue, ^{
// Download the photo
dispatch_async(dispatch_get_main_queue(), ^{
[self.spinner stopAnimating];
// Set the photo in the UI
}
});
});
}
Not the best method in the world but it will do thejob