Optimized Image Loading in a UIScrollView - iphone

I have a UIScrollView that has a set of images loaded side-by-side inside it. You can see an example of my app here: http://www.42restaurants.com. My problem comes in with memory usage. I want to lazy load the images as they are about to appear on the screen and unload images that aren't on screen. As you can see in the code I work out at a minimum which image I need to load and then assign the loading portion to an NSOperation and place it on an NSOperationQueue. Everything works great apart from a jerky scrolling experience.
I don't know if anyone has any ideas as to how I can make this even more optimized, so that the loading time of each image is minimized or so that the scrolling is less jerky.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self manageThumbs];
}
- (void) manageThumbs{
int centerIndex = [self centerThumbIndex];
if(lastCenterIndex == centerIndex){
return;
}
if(centerIndex >= totalThumbs){
return;
}
NSRange unloadRange;
NSRange loadRange;
int totalChange = lastCenterIndex - centerIndex;
if(totalChange > 0){ //scrolling backwards
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex - 5;
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex + 6;
}else if(totalChange < 0){ //scrolling forwards
unloadRange.length = fabsf(totalChange);
unloadRange.location = centerIndex - 6;
loadRange.length = fabsf(totalChange);
loadRange.location = centerIndex + 5;
}
[self unloadImages:unloadRange];
[self loadImages:loadRange];
lastCenterIndex = centerIndex;
return;
}
- (void) unloadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(thumbView.loaded){
UnloadImageOperation *unloadOperation = [[UnloadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:unloadOperation];
[unloadOperation release];
}
}
}
}
- (void) loadImages:(NSRange)range{
UIScrollView *scrollView = (UIScrollView *)[[self.view subviews] objectAtIndex:0];
for(int i = 0; i < range.length && range.location + i < [scrollView.subviews count]; i++){
UIView *subview = [scrollView.subviews objectAtIndex:(range.location + i)];
if(subview != nil && [subview isKindOfClass:[ThumbnailView class]]){
ThumbnailView *thumbView = (ThumbnailView *)subview;
if(!thumbView.loaded){
LoadImageOperation *loadOperation = [[LoadImageOperation alloc] initWithOperableImage:thumbView];
[queue addOperation:loadOperation];
[loadOperation release];
}
}
}
}
EDIT:
Thanks for the really great responses. Here is my NSOperation code and ThumbnailView code. I tried a couple of things over the weekend but I only managed to improve performance by suspending the operation queue during scrolling and resuming it when scrolling is finished.
Here are my code snippets:
//In the init method
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:4];
//In the thumbnail view the loadImage and unloadImage methods
- (void) loadImage{
if(!loaded){
NSString *filename = [NSString stringWithFormat:#"%03d-cover-front", recipe.identifier, recipe.identifier];
NSString *directory = [NSString stringWithFormat:#"RestaurantContent/%03d", recipe.identifier];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:#"png" inDirectory:directory];
UIImage *image = [UIImage imageWithContentsOfFile:path];
imageView = [[ImageView alloc] initWithImage:image andFrame:CGRectMake(0.0f, 0.0f, 176.0f, 262.0f)];
[self addSubview:imageView];
[self sendSubviewToBack:imageView];
[imageView release];
loaded = YES;
}
}
- (void) unloadImage{
if(loaded){
[imageView removeFromSuperview];
imageView = nil;
loaded = NO;
}
}
Then my load and unload operations:
- (id) initWithOperableImage:(id<OperableImage>) anOperableImage{
self = [super init];
if (self != nil) {
self.image = anOperableImage;
}
return self;
}
//This is the main method in the load image operation
- (void)main {
[image loadImage];
}
//This is the main method in the unload image operation
- (void)main {
[image unloadImage];
}

I'm a little puzzled by the "jerky" scrolling. Since NSOperationQueue runs operations on separate thread(s) I'd have expected at worst you might see empty UIImageViews showing up on the screen.
First and foremost I'd be looking for things that are impacting the processor significantly as NSOperation alone should not interfere with the main thread. Secondly I'd be looking for details surrounding the NSOperation setup and execution that might be causing locking and syncing issues which could interrupt the main thread and therefore impact scrolling.
A few items to consider:
Try loading your ThumbnailView's with a single image at the start and disabling the NSOperation queuing (just skip everything following the "if loaded" check. This will give you an immediate idea whether the NSOperation code is impacting performance.
Keep in mind that -scrollViewDidScroll: can occur many times during the course of a single scroll action. Depending on how for the scroll moves and how your -centerThumbIndex is implemented you might be attempting to queue the same actions multiple times. If you've accounted for this in your -initWithOperableImage or -loaded then its possible you code here is causing sync/lock issues (see 3 below). You should track whether an NSOperation has been initiated using an "atomic" property on the ThumbnailView instance. Prevent queuing another operation if that property is set and only unset that property (along with loaded) at the end of the NSOperation processes.
Since NSOperationQueue operates in its own thread(s) make sure that none of your code executing within the NSOperation is syncing or locking to the main thread. This would eliminate all of the advantages of using the NSOperationQueue.
Make sure your "unload" operation has a lower priority than your "load" operation, since the priority is the user experience first, memory conservation second.
Make sure you keep enough thumbnails for at least a page or two forward and back so that if NSOperationQueue falls behind, you have a high margin of error before blank thumbnails become visible.
Make sure your load operation is only loading a "pre-scaled" thumbnail and not loading a full size image and rescaling or processing. This would be a lot of extra overhead in the middle of a scrolling action. Go even further and make sure you've converted them to PNG16 without an alpha channel. This will give at least a (4:1) reduction in size with hopefully no detectable change in the visual image. Also consider using PVRTC format images which will take the size down even further (8:1 reduction). This will greatly reduced the time it takes to read the images from "disk".
I apologize if any of this doesn't make sense. I don't see any issues with the code you've posted and problems are more likely to be occurring in your NSOperation or ThumbnailView class implementations. Without reviewing that code, I may not be describing the conditions effectively.
I would recommend posting your NSOperation code for loading and unloading and at least enough of the ThumbnailView to understand how it interacts with the NSOperation instances.
Hope this helped to some degree,
Barney

One option, although less visually pleasing, is to only load images when the scrolling stops.
Set a flag to disable image loading in:
-scrollViewWillBeginDragging:
Re-enable loading images when the scrolling stops using the:
-scrollViewDidEndDragging:willDecelerate:
UIScrollViewDelegate method. When the willDecelerate: parameter is NO, the movement has stopped.

the problem is here:
UIImage *image = [UIImage imageWithContentsOfFile:path];
It seems that threaded or not when you load a file from disk (which maybe that happens on the main thread regardless, I'm not totally sure) everything stalls. You normally don't see this in other situations because you don't have such a large area moving if any at all.

While researching this problem, I found two more resources that may be of interest:
Check out the iPhone sample project "PageControl": http://developer.apple.com/iphone/library/samplecode/PageControl/index.html
It lazy loads view controllers in a UIScrollView.
and -
Check out the cocoa touch lib: http://github.com/facebook/three20 which has a 'TTPhotoViewController' class that lazy loads photos/thumbnails from web/disk.

Set shouldRasterize = YES for the sub content view adde to the scrollview. It is seen to remove the jerky behavior of custom created scroll view like a charm. :)
Also do some profiling using the instruments in the Xcode. Go through the tutorials created for profiling by Ray Wenderlich it helped me a lot.

Related

Having Trouble With GCD and Loading Thumbnails in TableView

I have the following code that attempts to load a row of thumbnails in a tableview asynchronously:
for (int i = 0; i < totalThumbnails; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
__block GraphicView *graphicView;
__block Graphic *graphic;
dispatch_async(dispatch_get_main_queue(),
^{
graphicView = [[tableViewCell.contentView.subviews objectAtIndex:i] retain];
graphic = [[self.thumbnailCache objectForKey: [NSNumber numberWithInt:startingThumbnailIndex + i]] retain];
if (!graphic)
{
graphic = [[self graphicWithType:startingThumbnailIndex + i] retain];
[self.thumbnailCache setObject: graphic forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
}
[graphicView setGraphic:graphic maximumDimension:self.cellDimension];
});
[graphicView setNeedsDisplay];
dispatch_async(dispatch_get_main_queue(),
^{
CGRect graphicViewFrame = graphicView.frame;
graphicViewFrame.origin.x = ((self.cellDimension - graphicViewFrame.size.width) / 2) + (i * self.cellDimension);
graphicViewFrame.origin.y = (self.cellDimension - graphicViewFrame.size.height) / 2;
graphicView.frame = graphicViewFrame;
});
[graphicView release];
[graphic release];
});
}
However when I run the code I get a bad access at this line: [graphicView setNeedsDisplay]; It's worth mentioning that the code works fine when I have it set up like this:
for (int i = 0; i < totalThumbnails; i++)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
dispatch_async(dispatch_get_main_queue(),
^{
//put all the code here
});
}
It works fine and the UITableView loads asynchronously when it's first called, however the scrolling is still really choppy.
So I'd like to get the to get the first bit of code to work so I can get the drawing done in the global thread instead of the main thread (which I assume will fix the choppy scrolling?).
Since iOS4 drawing is able to be done asynchronously so I don't believe that is the problem. Possibly I'm misusing the __Block type?
Anyone know how I can get this to work?
You completely misunderstand how to use GCD. Looking at your code:
__block GraphicView *graphicView;
Your variable here is not initialised to nil. It is unsafe to send messages to.
__block Graphic *graphic;
dispatch_async(dispatch_get_main_queue(),
^{
//statements
});
Your dispatch statement here returns immediately. The system works for you to spin this task off on a different thread. Before, or perhaps at the same time as, the above statements are executed we move on to the next line of execution here...
[graphicView setNeedsDisplay];
At this point graphic view may or may not have been initialised by your dispatch statement above. Most likely not as there wont have been time. As it still hasn't been initialised it points to random memory and hence trying to send messages to it causes EXC_BAD_ACCESS.
If you want to draw cell contents asynchronously (or pre-render images or whatever.) I thouroughly reccommend watching WWDC 2012 session 211 "Building Concurrent User Interfaces on iOS". They do almost exactly what you seem to be attempting to do and explain all the pitfalls you can run into.
I think the issue is because you are trying to re-draw the UIView on a working thread. You should move this:
[graphicView setNeedsDisplay];
To the main queue.

UIAlertView Rendering Error

I have been working on an app for a couple of months now, but have finally run into an issue that I can't solve myself, and can't find anything on the internet to help.
I am using several normal UIAlertViews, in my app. Some have 2 buttons, some have 3 buttons, and a couple have 2 buttons and a text field. However all have the same issue. When you call [someAlertView show]; the alert view appears as normal, but then suddenly its graphics context seems to get corrupted as you can see from the screenshot.
This happens on both iPhone and iPad simulators (both 5.0 and 5.1), and happens on an iPad and iPhone4S device as well.
The image showing through is whatever happens to be behind the alertView.
The Alert still works, I can click the buttons, type in the text field, then when it dismisses the delegate methods are called correctly and everything goes back to normal. When the alertView appears again, the same thing happens.
The view behind the alert is a custom UIScrollView subclass with a content size of approximately 4000 pixels by 1000 with a UIImage as the background. The png file is mostly transparent, so is only about 80kB in memory size, and the phone is having no issues rendering it - the scroll view is still fully responsive and not slow.
It also has a CADisplayLink timer attached to it as part of the subclass. I have tried disabling this just before the alertView is shown, but it makes no difference so I am doubtful that is the issue.
This app is a partial rewrite of one I made for a university project, and that one could display UIAlertViews over the top of a scrollView of the same size and subclass without issue. The difference between this app and that one is that in my old app, I had subclassed UIAlertView to add extra things such as a pickerView, however I decided that I didn't like the way it looked so moved everything out of the alert and am just sticking with a standard UIAlertView.
This is how the alertView in the screenshot is called:
- (IBAction)loadSimulation:(id)sender {
importAlert = [[UIAlertView alloc] initWithTitle:#"Load Simulation" message:#"Enter Filename:" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Load", nil];
[importAlert setAlertViewStyle:UIAlertViewStylePlainTextInput];
[importAlert showPausingSimulation:self.simulationView]; //Calling [importAlert show]; makes no difference.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
[self hideOrganiser]; //Not an issue as the problem occurs on iPad as well.
}
}
With this being the categorised AlertView to add the ability to stop the scrollViews CADisplay link.
#interface UIAlertView(pauseDisplayLink)
- (void)showPausingSimulation:(UILogicSimulatorView*)simulationView;
#end
#implementation UIAlertView(pauseDisplayLink)
- (void)showPausingSimulation:(UILogicSimulatorView *)simulationView {
[simulationView stopRunning];
[simulationView removeDisplayLink]; //displayLink needs to be removed from the run loop, otherwise it will keep going in the background and get corrupted.
[self show];
}
I get no memory warnings when this happens, so I am doubtful it is due to lack of resources.
Has anyone come across an issue like this before? If you need further information I can try to provide it, but I am limited in what code I can post. Any help would be appreciated, I've been trying to solve this for two weeks and can't figure it out.
Edit:
It appears that it is not the AlertView at all (or rather it is not just the alertView), as the problem goes away when I remove the scroll view behind it, so there must be some issue between the two. This is the code for my UIScrollView subclass:
.h file:
#import
#import
#class ECSimulatorController;
#interface UILogicSimulatorView : UIScrollView {
CADisplayLink *displayLink;
NSInteger _updateRate;
ECSimulatorController* _hostName;
}
#property (nonatomic) NSInteger updateRate;
#property (nonatomic, strong) ECSimulatorController* hostName;
- (void) removeDisplayLink;
- (void) reAddDisplayLink;
- (void) displayUpdated:(CADisplayLink*)timer;
- (void) startRunning;
- (void) stopRunning;
- (void) refreshRate:(NSInteger)rate;
- (void) setHost:(id)host;
- (void)setMinimumNumberOfTouches:(NSInteger)touches;
- (void)setMaximumNumberOfTouches:(NSInteger)touches;
#end
.m file:
#import "UILogicSimulatorView.h"
#import "ECSimulatorController.h"
#import <QuartzCore/QuartzCore.h>
#implementation UILogicSimulatorView
#synthesize updateRate = _updateRate;
#synthesize hostName = _hostName;
- (void)reAddDisplayLink {
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; //allows the display link to be re-added to the run loop after having been removed.
}
- (void)removeDisplayLink {
[displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; //allows the display link to be removed from the Run loop without deleting it. Removing it is essential to prevent corruption between the games and the simulator as both use CADisplay link, and only one can be in the run loop at a given moment.
}
- (void)startRunning {
[self refreshRate:self.updateRate];
[displayLink setPaused:NO];
}
- (void)refreshRate:(NSInteger)rate {
if (rate > 59) {
rate = 59; //prevent the rate from being set too an undefined value.
}
NSInteger frameInterval = 60 - rate; //rate is the number of frames to skip. There are 60FPS, so this converts to frame interval.
[displayLink setFrameInterval:frameInterval];
}
- (void)stopRunning {
[displayLink setPaused:YES];
}
- (void)displayUpdated:(CADisplayLink*)timer {
//call the function that the snakeController host needs to update
[self.hostName updateStates];
}
- (void)setHost:(ECSimulatorController*)host;
{
self.hostName = host; //Host allows the CADisplay link to call a selector in the object which created this one.
}
- (id)initWithFrame:(CGRect)frame
{
//Locates the UIScrollView's gesture recogniser
if(self = [super initWithFrame:frame])
{
[self setMinimumNumberOfTouches:2];
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayUpdated:)]; //CADisplayLink will update the logic gate states.
self.updateRate = 1;
[displayLink setPaused:YES];
}
return self;
}
- (void)setMinimumNumberOfTouches:(NSInteger)touches{
for (UIGestureRecognizer *gestureRecognizer in [self gestureRecognizers])
{
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
//Changes the minimum number of touches to 'touches'. This allows the UIPanGestureRecogniser in the object which created this one to work with one finger.
[(UIPanGestureRecognizer*)gestureRecognizer setMinimumNumberOfTouches:touches];
}
}
}
- (void)setMaximumNumberOfTouches:(NSInteger)touches{
for (UIGestureRecognizer *gestureRecognizer in [self gestureRecognizers])
{
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
//Changes the maximum number of touches to 'touches'. This allows the UIPanGestureRecogniser in the object which created this one to work with one finger.
[(UIPanGestureRecognizer*)gestureRecognizer setMaximumNumberOfTouches:touches];
}
}
}
#end
Well, I have managed to come up a solution to this. Really it is probably just masking the issue rather than finding the route cause, but at this point I will take it.
First some code:
#interface UIView (ViewCapture)
- (UIImage*)captureView;
- (UIImage*)captureViewInRect:(CGRect)rect;
#end
#implementation UIView (ViewCapture)
- (UIImage*)captureView {
return [self captureViewInRect:self.frame];
}
- (UIImage*)captureViewInRect:(CGRect)rect
{
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:context];
UIImage *screenShot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return screenShot;
}
#end
- (void)showPausingSimulation:(UILogicSimulatorView *)simulationView {
[simulationView stopRunning];
UIView* superView = simulationView.superview;
CGPoint oldOffset = simulationView.contentOffset;
for (UIView* subview in simulationView.subviews) {
//offset subviews so they appear when content offset is (0,0)
CGRect frame = subview.frame;
frame.origin.x -= oldOffset.x;
frame.origin.y -= oldOffset.y;
subview.frame = frame;
}
simulationView.contentOffset = CGPointZero; //set the offset to (0,0)
UIImage* image = [simulationView captureView]; //Capture the frame of the scrollview
simulationView.contentOffset = oldOffset; //restore the old offset
for (UIView* subview in simulationView.subviews) {
//Restore the original positions of the subviews
CGRect frame = subview.frame;
frame.origin.x += oldOffset.x;
frame.origin.y += oldOffset.y;
subview.frame = frame;
}
[simulationView setHidden:YES];
UIImageView* imageView = [[UIImageView alloc] initWithFrame:simulationView.frame];
[imageView setImage:image];
[imageView setTag:999];
[superView addSubview:imageView];
[imageView setHidden:NO];
superView = nil;
imageView = nil;
image = nil;
[self show];
}
- (void)dismissUnpausingSimulation:(UILogicSimulatorView *)simulationView {
UIView* superView = simulationView.superview;
UIImageView* imageView = (UIImageView*)[superView viewWithTag:999];
[imageView removeFromSuperview];
imageView = nil;
superView = nil;
[simulationView setHidden:NO];
[simulationView startRunning];
}
Then modifying the dismiss delegate method in my class to have this line:
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
[alertView dismissUnpausingSimulation:self.simulationView];
...
When the alert view is called, but before it is shown, I need to hide the simulator to prevent it corrupting the alert. However just hiding it is ugly as then all is visible behind is a empty view.
To fix this, I first make a UIImage from the simulator views graphics context. I then create a UIImageView with the same frame as the simulator and set the UIImage as its image.
I then hide the simulator view (curing the alert issue), and add my new UIImageView to the simulators superview. I also set the tag of the image view so I can find it later.
When the alert dismisses, the image view is then recovered based on its tag, and removed from its superview. The simulator is then unhidden.
The result is that the rendering issue is gone.
I know its too late for an answer to this question. Lately I had experianced this very same issue.
My Case:
Added couple of custom UIViews with background images and some controlls to the scroll view with shadow effect. I had also set the shadowOffset.
The Solution:
After some step by step analysis, I found out that setting the setShadowOpacity caused The rendering problem for me. When i commented that line of code, it cured the UIAlertView back to normal appearance.
More:
To make sure, I created a new project mimicing the original ui with shadowOpacity. But it didnt caused the rendering problem as i expected. So I am not sure about the root cause. For me it was setShadowOpacity.

how to display UIActivityIndicatorView BEFORE rotation begins

I'd like to display an activity indicator BEFORE the work undertaken by willAnimateRotationToInterfaceOrientation:duration: begins. Most of the time in my app, this work is quickly completed and there would be no need for an activity indicator, but occasionally (first rotation, i.e. before I have cached data, when working with a large file) there can be a noticeable delay. Rather than re-architect my app to cope with this uncommon case, I'd rather just show the UIActivityIndicatorView while the app generates a cache and updates the display.
The problem is (or seems to be) that the display is not updated between the willRotateToInterfaceOrientation:duration and the willAnimateRotationToInterfaceOrientation:duration: method. So asking iOS to show UIActivityIndicator view in willRotate method doesn't actually affect the display until after the willAnimateRotation method.
The following code illustrates the issue. When run, the activity indicator appears only very briefly and AFTER the simulateHardWorkNeededToGetDisplayInShapeBeforeRotation method has completed.
Am I missing something obvious? And if not, any smart ideas as to how I could work around this issue?
Update: While suggestions about farming the heavy lifting off to another thread etc. are generally helpful, in my particular case I kind of do want to block the main thread to do my lifting. In the app, I have a tableView all of whose heights need to be recalculated. When - which is not a very common use case or I wouldn't even be considering this approach - there are very many rows, all the new heights are calculated (and then cached) during a [tableView reloadData]. If I farm the lifting off and let the rotate proceed, then after the rotate and before the lifting, my tableView hasn't been re-loaded. In the portrait to landscape case, for example, it doesn't occupy the full width. Of course, there are other workarounds, e.g. building a tableView with just a few rows prior to the rotate and then reloading the real one over that etc.
Example code to illustrate the issue:
#implementation ActivityIndicatorViewController
#synthesize activityIndicatorView = _pgActivityIndicatorView;
#synthesize label = _pgLabel;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
[self hideActivityIndicatorView];
}
- (void) simulateHardWorkNeededToGetDisplayInShapeBeforeRotation;
{
NSLog(#"Starting simulated work");
NSDate* date = [NSDate date];
while (fabs([date timeIntervalSinceNow]) < 2.0)
{
//
}
NSLog(#"Finished simulated work");
}
- (void) showActivityIndicatorView;
{
NSLog(#"showActivity");
if (![self activityIndicatorView])
{
UIActivityIndicatorView* activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self setActivityIndicatorView:activityIndicatorView];
[[self activityIndicatorView] setCenter:[[self view] center]];
[[self activityIndicatorView] startAnimating];
[[self view] addSubview: [self activityIndicatorView]];
}
// in shipping code, an animation with delay would be used to ensure no indicator would show in the good cases
[[self activityIndicatorView] setHidden:NO];
}
- (void) hideActivityIndicatorView;
{
NSLog(#"hideActivity");
[[self activityIndicatorView] setHidden:YES];
}
- (void) dealloc;
{
[_pgActivityIndicatorView release];
[super dealloc];
}
- (void) viewDidLoad;
{
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(50.0, 50.0, 0.0, 0.0)];
[label setText:#"Activity Indicator and Rotate"];
[label setTextAlignment: UITextAlignmentCenter];
[label sizeToFit];
[[self view] addSubview:label];
[self setLabel:label];
[label release];
}
#end
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the hard work method before displaying the activity indicator.
To make this work, you need to push the hard work over to another thread. I would put the call to the hard work method in the willRotate... method. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method. I wouldn't bother with a didRotateFrom... method. I recommend reading the Threaded Programming Guide.
Edit in response to a comment: You can effectively block user interaction by having the willAnimateRotation... method put a non functioning interface on screen such as a view displaying a dark overlay over and the UIActivityIndicatorView. Then when the heavy lifting is done, this overlay is removed, and the interface becomes active again. Then the drawing code will have the opportunity to properly add and animate the activity indicator.
More digging (first in Matt Neuberg's Programming iPhone 4) and then this helpful question on forcing Core Animation to run its thread from stackoverflow and I have a solution that seems to be working well. Both Neuberg and Apple issue strong caution about this approach because of the potential for unwelcome side effects. In testing so far, it seems to be OK for my particular case.
Changing the code above as follows implements the change. The key addition is [CATransaction flush], forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended until after the willAnimateRotationToInterfaceOrientation:duration method completes.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
[CATransaction flush]; // this starts the animation right away, w/o waiting for end of the run loop
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
[self hideActivityIndicatorView];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
}
Try performing you work on a second thread after showing the activity view.
[self showActivityIndicatorView];
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.01];
Either execute the heavy lifting in a background thread and post the results in the foreground thread to update the UI (UIKit is only thread safe since iOS 4.0):
[self performSelectorInBackground:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil]
Or you can schedule the heavy lifting method to be executed after the rotation took place:
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.4]
But these are only hacks and the real solution is to have proper background processing if your UI needs heavy processing to get updated, may it be in portrait or landscape. NSOperation and NSOperationQueue is a good place to start.

NSThread problem in iOS

I have an app that loads multiple thumbnail images into a UIScrollVIew. This is a lengthy operation, and so as not to block up the display of the rest of the UI, I am running it in a separate thread. This works fine the first time at application launch, but later a new set of images needs to be loaded into the UIScrollView. When I detach a thread a second time the app crashes (sometimes). Code follows:
// this call is in a separate method
[NSThread detachNewThreadSelector:#selector(addThumbnailsToScrollView) toTarget:self withObject:nil];
// this is the main entry point for the thread
- (void) addThumbnailsToScrollView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
// now place all the thumb views as subviews of the scroll view
float xPosition = THUMB_H_PADDING;
int pageIndex = 0;
for (Page *page in self.pages) {
// get the page's bitmap image and scale it to thumbnail size
NSString *name = [page valueForKey:#"pageBackground"];
NSString *basePath = [[NSBundle mainBundle] pathForResource:page.pageBackground ofType:#"jpg" inDirectory:nil];
UIImage *thumbImage = [UIImage imageWithContentsOfFile:basePath];
thumbImage = [thumbImage imageScaledToSize:CGSizeMake(80, 100)];
// create a ThumbImageView for each page and add it to the thumbnailScrollView
if (thumbImage) {
ThumbImageView *thumbView = [[ThumbImageView alloc] initWithImage:thumbImage];
[thumbView setDelegate:self];
[thumbView setImageName:name];
[thumbView setImageSize:CGSizeMake(80, 100)];
[thumbView setPageIndex:pageIndex];
pageIndex ++;
CGRect frame = [thumbView frame];
frame.origin.y = 0;
frame.origin.x = xPosition;
[thumbView setFrame:frame];
[thumbnailPagesScrollView addSubview:thumbView];
[thumbView release];
xPosition += (frame.size.width + THUMB_H_PADDING);
}
}
[self hightlightThumbnailPageAtIndex:0];
[(UIActivityIndicatorView *)[thumbnailPagesScrollView.superview viewWithTag:100] stopAnimating];
[pool release]; // Release the objects in the pool.
}
I thought that a detached thread exits as soon as the main entry routine was completed. Wouldn't the second call to detach a thread be a new thread? Why is the app crashing, but sometimes not?
Thanks
Jk
You cannot touch UIKit (meaning UIScrollVIew) in a secondary thread - you need to reorganize so that the fetch takes place in a secondary thread but you make a NSData object (containing the image binary) available to your primary thread for each thumbnail so that it can do everything related to actually displaying them.
Apple repeatedly warn in documentation that UIKit is not thread-safe.
I would suggest adding the thumbView to the thumbnailPagesScrollView on the main thread rather than a separate thread. There might be issues on the retain count of the object across threads. There is a convenience method performSelectorOnMainThread I think it is to do that. You could pass thumbView to that and then add it to the subview.
Alternatively you could do the whole if statement on the main thread as thats not the thing that will interrupt the user.
Also with your activity indicator this should be stopped on the main thread. Everything UI related should be done on the main thread.

can not get setNeedsDisplay to work within a loop for UIImageView

A frequently answered question, I'm afraid but I am pretty much in the dark on this.
Within my view controller I have the following method to switch back and forward between two images a total of 5 times
- (IBAction)cycle{
BOOL select1;
select1=YES;
UIImage *image1 = [UIImage imageNamed:#"image1.png"];
UIImage *image2 = [UIImage imageNamed:#"image2.png"];
for (int i=0; i<5; i++) {
if (select1){
[imageview setImage:image1];
} else {
[imageview setImage:image2];
}
[NSThread sleepForTimeInterval:1.0];
[imageview setNeedsDisplay];
}
}
The problem is that the setNeedsDisplay message does not work within the loop and the view is only updated when the method quits.
Is there any thing I can do here? Is the approach feasible or am I completely down the wrong track. This is very much a test program (I am new to this language) but it would be useful to control something like this programmatically. The next step app will implement randomly changing times between picture changes.
Can anyone help me on this?
Wrong track. Calling
[NSThread sleepForTimeInterval:1.0];
Is only trying to put the main thread asleep. The problem is that this is the thread that's actually does the drawing. You need to end what you're doing and give control bakc to the NS runtime so that it can update gui elements.
Try using NSTimer to create a scheduled timer. Have it repeat with the time interval that you want. All that you then have to do is set the image:
if (select1){
[imageview setImage:image1];
} else {
[imageview setImage:image2];
}
there's no need for
[imageview setNeedsDisplay];
As the imageview will handle that for itself.