IPhone SDK: Problem with lazy loading scrollView pictures - iphone

My app involves a scrollView containing imageViews chosen by the user through a modified ELCImagePicker. The chosen pictures will typically be high quality photos at 5 MB+ and the user will usually choose at least a dozen pictures at a time. Currently, I am loading the photos as below:
-(void)loadViewWithPage: (int)page
{
if (page > 0 && page < [Album count]) {
[scrollView addSubview:[Album objectAtIndex:page]];
}
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth) / pageWidth) + 1;
[self loadViewWithPage:page + 1];
}
Where Album is where the photos are stored as imageViews.
This works great when the user is not trying to break the app and scroll through the photos one at a time, but fails miserably when he/she tries to scroll through the entire selection. The pages are blank unless the user stops after each photo. I tried using scrollViewDidScroll ala PageControl sample, but since the photos are all huge the lag is very visible.
Is there any way of loading the photos smoothly?

I had a similar situation and dealt with it by creating a custom subclass of NSOperation which would load the images in a separate thread and then display them by calling a method on the main thread.
While the image was loading I displayed a UIActivityView
Hope that helps.

if you use scrollViewDidEndDecelerating it will only fire when the scrollview stops. I would use scrollViewDidScroll for that. (it's also used in their examples)

Related

ScrollView with large images scrolls too slowly

When I am scrolling images frequently in a UIScrollView then after some images, the next image takes time to load... it's not taking too much time but looks odd.
Suppose I have 27 images in a scrollView. When I start to scroll these images, for 1 or 2 images it scrolls smoothly but when I scroll again to see the 3rd image it takes time to load. Then when I start the images scrolling again from the 3rd image, it behaves like before.
I can't load all 27 images at a time or my app crashes.
When I slowly scroll the scrollview then I don't have this problem.
My code is below:
//Taking image view for 27 images;
int x=0;
for(int i = 1; i<=27; i++) {
imageView = [[UIImageView alloc] init];
imageView .frame = CGRectMake(x,0,768,1024);
imageView.tag=i;
imageView.image=nil;
imageView.userInteractionEnabled=YES;
[contentView addSubview:imageView];
x+=768;
}
//setContentOffset of the scrollView -->ContentView
[contentView setContentOffset: CGPointMake((imageNumber-1)*768, 0) animated: YES];
//desire image which i want to see from the start of the scrollview
pageNumber=imageNumber;
int pageBefore=pageNumber-1;
int pageAfter=pageNumber+1;
//Views for image
for( UIImageView * views in [contentView subviews]){
if(views.tag==pageNumber){
if(views.image==nil){
NSLog(#"entering");
views.image=[UIImage imageNamed:[ NSString stringWithFormat:#"%d.jpg",pageNumber]];
[views.image release];
}
}
if(views.tag==pageBefore){
if(views.image==nil){
views.image=[UIImage imageNamed:[ NSString stringWithFormat:#"%d.jpg",pageBefore]];
[views.image release];
}
}
if(views.tag==pageAfter){
if(views.image==nil){
views.image=[UIImage imageNamed:[ NSString stringWithFormat:#"%d.jpg",pageAfter]];
[views.image release];
}
}
My alarm bells rang when I saw this;
imageView .frame = CGRectMake(x,0,768,1024);
Apart from the space before .frame, are you saying that your images are 768x1024? That's HUGE and I suspect your problems are memory ones rather than code ones.
Be aware that in particular, using UIImage imageNamed: is likely to cause grief with such large images as that method caches the images in memory. You may wish to consider using alternative methods that load the image from a file each time.
You should try use the EGOImageView, it has caching build in which might help with your performance issues. You can implement a placeholder image to show the user that an image is being prepared for viewing. The image will load in another thread before being displayed, giving you smoother scrolling performance. The EGOImageView is part of the EGOImageLoading library.
https://github.com/tastefulworks/EGOImageLoading
As an alternative you could create your own lazy loading mechanism to increase scrolling performance. E.g. once a user stops scrolling for a second, start loading the image, otherwise display placeholder image if not yet the correct image is cached.
Edit: when thinking more about this issue, I realize caching won't help much (since you already load image from disk), but the asynchronous loading of images should help with the scroll performance, so make use of NSThread or NSOperation to load the image in a background thread, then notify the main thread that the image is loaded and ready for display.

How to update ui in background without losing ui responsiveness? [duplicate]

I am creating a application which displays 8 thumbnails per page and it can have n pages. Each of these thumbnails are UIViews and are added to UIScrollView. However i have implemented Paging using the Apple sample code.
The prob:
Each thumbnail(UIView) takes 150
millisecs to be created and added to
scroll view
Hence for 3 pages it takes awful
huge time to be created and added to
the UI Scrollview.
At this point the scroll view is not very respsonsive and it is very jerky and gives a bad user experience
How can i create the thumbnails and add them to UIScrollview without affecting the touch responsiveness? I want them to run independent of the main thread which is resposible for handling touch events (i suppose).
Also i would like to mention that when a thumbnail is created i trigger a Async download of the image and the delegate method is called when download is complete.
Let me know the the options i have to make this more responsive and update UI without affecting the touch operations. The page control works fine with lazy loading of the grid of thumbnails.
TIA,
Praveen S
Grand Central Dispatch is easy to use for background loading. But GCD is only for after iOS4. If you have to support iOS3, performSelectorInBackground/performSelectorOnMainThread or NSOperationQueue are helpful.
And, be careful almost UIKit classes are not thread-safe except drawing to a graphics context. For example, UIScrollView is not thread-safe, UIImage imageNamed: is not thread-safe, but UIImage imageWithContentsOfFile: is thread-safe.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
dispatch_apply([thumbnails count], concurrentQueue, ^(size_t index) {
Thumbnail *thumbnail = [thumbnails objectAtIndex:index];
thumbnail.image = [UIImage imageWithContentsOfFile:thumbnail.url];
dispatch_sync(mainQueue, ^{
/* update UIScrollView using thumbnail. It is safe because this block is on main thread. */
});
}
/* dispatch_apply waits until all blocks are done */
dispatch_async(mainQueue, ^{
/* do for all done. */
});
}
I was having a similar problem.
What i did was at an instance i kept only 3 pages in the memory and cleared remaining all.
If suppose there are 3 screens s1, s2, s3. And the user is viewing s2. Whenever he scrolls to s3 i will remove s1 and load a new page s4.
So that the users will have a better experience. And less memory will be occupied.
Whether you are using a subview or a separate ViewController for each "page" or element of the Scrollview, the jerkiness or poor performance can be helped by changing the location of your code.
Specifically the apple sample code for a scrollview with pagecontrol has something like this:
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
However, that code appears in their sample in the method "scrollViewDidScroll". It's trying to do multiple heavy lifting by both scrolling and loading at the same time. Even if your images are local this is nasty.
If you move this and related code including a reference to the current page to "scrollViewDidEndDecelerating" the jerkiness of the interface is resolved because the loading happens while the scrollview is no longer moving.

How to make ui responsive all the time and do background updating?

I am creating a application which displays 8 thumbnails per page and it can have n pages. Each of these thumbnails are UIViews and are added to UIScrollView. However i have implemented Paging using the Apple sample code.
The prob:
Each thumbnail(UIView) takes 150
millisecs to be created and added to
scroll view
Hence for 3 pages it takes awful
huge time to be created and added to
the UI Scrollview.
At this point the scroll view is not very respsonsive and it is very jerky and gives a bad user experience
How can i create the thumbnails and add them to UIScrollview without affecting the touch responsiveness? I want them to run independent of the main thread which is resposible for handling touch events (i suppose).
Also i would like to mention that when a thumbnail is created i trigger a Async download of the image and the delegate method is called when download is complete.
Let me know the the options i have to make this more responsive and update UI without affecting the touch operations. The page control works fine with lazy loading of the grid of thumbnails.
TIA,
Praveen S
Grand Central Dispatch is easy to use for background loading. But GCD is only for after iOS4. If you have to support iOS3, performSelectorInBackground/performSelectorOnMainThread or NSOperationQueue are helpful.
And, be careful almost UIKit classes are not thread-safe except drawing to a graphics context. For example, UIScrollView is not thread-safe, UIImage imageNamed: is not thread-safe, but UIImage imageWithContentsOfFile: is thread-safe.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
dispatch_apply([thumbnails count], concurrentQueue, ^(size_t index) {
Thumbnail *thumbnail = [thumbnails objectAtIndex:index];
thumbnail.image = [UIImage imageWithContentsOfFile:thumbnail.url];
dispatch_sync(mainQueue, ^{
/* update UIScrollView using thumbnail. It is safe because this block is on main thread. */
});
}
/* dispatch_apply waits until all blocks are done */
dispatch_async(mainQueue, ^{
/* do for all done. */
});
}
I was having a similar problem.
What i did was at an instance i kept only 3 pages in the memory and cleared remaining all.
If suppose there are 3 screens s1, s2, s3. And the user is viewing s2. Whenever he scrolls to s3 i will remove s1 and load a new page s4.
So that the users will have a better experience. And less memory will be occupied.
Whether you are using a subview or a separate ViewController for each "page" or element of the Scrollview, the jerkiness or poor performance can be helped by changing the location of your code.
Specifically the apple sample code for a scrollview with pagecontrol has something like this:
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
However, that code appears in their sample in the method "scrollViewDidScroll". It's trying to do multiple heavy lifting by both scrolling and loading at the same time. Even if your images are local this is nasty.
If you move this and related code including a reference to the current page to "scrollViewDidEndDecelerating" the jerkiness of the interface is resolved because the loading happens while the scrollview is no longer moving.

Detecting which part of a scrollview is on screen

I have scroll view to which i add a variable number of uitextviews,so that the user can see each text with a swipe.Its working all fine.But i want to display a label when the user reaches the last text.I dont know how to detect whether the user has reached the last textview.I hope im clear enough with my question.Thanks
The typical way of doing this is calculating the page the user is on and evaluating that ( this is to utilise page Controls) This is done easily with this:
int currentPage = floor(scrollView.contentOffset.x / scrollView.frame.size.width)+1;
If you simply just want to know if the user is on the last page...
if(scrollView.contentOffset.x >= scrollView.contentSize.width-scrollView.frame.size.width)
{
NSLog(#"USER IS ON THE LAST PAGE");
}
You would typically put this in:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
or:
- (void)scrollViewDidScroll:(UIScrollView *)sender;
depending on what your looking for.

Lazy load pages in UIScrollView

I have a UIScrollView that contains large images and am using paging to scroll between images. In order to save memory, I am loading only one image before and after the currently visible one and loading/releasing new images after a scroll has completed.
The problem occurs when one scrolls quickly and scrollViewDidEndDecelerating is not called.
How can I detect continuous scrolling?
I could check item location on every scrollViewDidScroll but this seems a bit heavy...
Perhaps a table view with custom cell content would work better for you, since it has a lot of logic built in for only loading cells that are visible as opposed to everything at once. There are lots of tutorials for how to manage table views in various ways.
My temporary solution is to cancel continuous scrolling by disabling scroll from when the user lifts the finger until scroll has completed.
-(void) scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
[scrollView setScrollEnabled:NO];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[scrollView setScrollEnabled:YES];
}
if you are still looking for a solution... this what I do, it works really good having a good performance, too.
- (void)scrollViewDidEndDragging:(UIScrollView *)sv willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
// load images and unload the ones that are not needed anymore
[self someMethod]
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sv
{
// load images and unload the ones that are not needed anymore
[self someMethod]
}
You have to check both scrollViewDidEndDecelerating and scrollViewDidScroll.
When to user swipes and releases it letting to have the "momentum" scroll the first will be called at the and. If the user decides to stop the scroll with his finger by tapping to the tableview (or scroll comes to the most bottom, but I am not sure about that) the second event is fired.
You can do something like this
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 1.5) / pageWidth) + 1;
}
this method is called anytime the contentoffset changed whether programmatically or not.
From this method you can check to see if 'page' is the same as your current page, if not, you know you need to load something. The trick here is making images load without holding up the scrolling of the scrollview. That is where I am stuck.
the page calculation will change to next/previous when you are half way to the next page
As per my experience, #jd291 is correct; I am using following callbacks successfully for most places
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
but, the only case when a callback is not called is that you may not had set up the delegate properly.
The WWDC 2010 has talked about this. They do a reuse of the sub scrollviews, alloc the missing image view in scrollViewDidScroll:. The video may help:
Session 104 - Designing Apps with Scroll Views