iOS UIScrollView performance
ios, performance, uibutton, uiimage, uiscrollview
Solution
Here's a few recommendations:
1) First, understand that `scrollViewDidScroll:` will get called continuously, as the user scrolls. Not just once per page. So, I would make sure that you have logic that ensures that the real work involved in your loading is only triggered once per page.
Typically, I will keep a class ivar like `int lastPage`. Then, as `scrollViewDidScroll:` is called, I calculate the new current page. Only if it differs from the ivar do I trigger loading. Of course, then you need to save the dynamically calculated index (`currentPage` in your code) in your ivar.
2) The other thing is that I try not to do all the intensive work in the `scrollViewDidScroll:` method. I only trigger it there.
So, for example, if you take most of the code you posted and put it in a method called `loadAndReleasePages`, then you could do this in the `scrollViewDidScroll:` method, which defers the execution until after `scrollViewDidScroll:` finishes:
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage = (int)(offset.y / 322.0f);
if (currentPage != lastPage) {
lastPage = currentPage;
// we've changed pages, so load and release new content ...
// defer execution to keep scrolling responsive
[self performSelector: @selector(loadAndReleasePages) withObject: nil afterDelay:0];
}
}
This is code that I've used since early iOS versions, so you can certainly replace the `performSelector:` call with an asynchronous GCD method call, too. The point is not to do it inside the scroll view delegate callback.
3) Finally, you might want to experiment with slightly different algorithms for calculating when the scroll view has actually scrolled far enough that you want to load and release content. You currently use:
int currentPage=(int)(offset.y / 322.0f);
which will yield integer page numbers based on the way the `/` operator, and the `float` to `int` cast works. That may be fine. However, you might find that you want a slightly different algorithm, to trigger the loading at a slightly different point. For example, you might want to trigger the content load as the page has scrolled exactly 50% from one page to the next. Or you might want to trigger it only when you're almost completely off the first page (maybe 90%).
I believe that one scrolling intensive app I wrote actually did require me to tune the precise moment in the page scroll when I did the heavy resource loading. So, I used a slightly different rounding function to determine when the current page has changed.
You might play around with that, too.
Edit: after looking at your code a little more, I also see that the work you're doing is loading and scaling images. This is actually also a candidate for a background thread. You can load the `UIImage` from the filesystem, and do your scaling, on the background thread, and use GCD to finally set the button's background image (to the loaded image) and change its frame back on the UI thread.
`UIImage` is safe to use in background threads since iOS 4.0.
Problem
I'm trying to increase the scrolling performance of my `UIScrollView`. I have a lot of `UIButtons` on it (they could be hundreds): every button has a png image set as background. If I try to load the entire scroll when it appears, it takes too much time. Searching on the web, I've found a way to optimize it (loading and unloading pages while scrolling), but there's a little pause in scrolling everytime I have to load a new page. Do you have any advice to make it scroll smoothly? Below you can find my code. ``` - (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView { CGPoint offset = tmpScrollView.contentOffset; //322 is the height of 2*2 buttons (a page for me) int currentPage=(int)(offset.y / 322.0f); if(lastContentOffset>offset.y){ pageToRemove = currentPage+3; pageToAdd = currentPage-3; } else{ pageToRemove = currentPage-3; pageToAdd = currentPage+3; } //remove the buttons outside the range of the visible pages if(pageToRemove>=0 && pageToRemove<=numberOfPages && currentPage<=numberOfPages){ for (UIView *view in scrollView.subviews) { if ([view isKindOfClass:[UIButton class]]){ if(lastContentOffset<offset.y && view.frame.origin.y<pageToRemove*322){ [view removeFromSuperview]; } else if(lastContentOffset>offset.y && view.frame.origin.y>pageToRemove*322){ [view removeFromSuperview]; } } } } if(((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd) || (lastContentOffset>offset.y && lastPageToAdd-1==pageToAdd)) && pageToAdd>=0 && pageToAdd<=numberOfPages){ int tmpPage=0; if((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd)){ tmpPage=pageToAdd-1; } else{ tmpPage=pageToAdd; } //the images are inside the application folder NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; for(int i=0;i<4;i++){ UIButton* addButton=[[UIButton alloc] init]; addButton.layer.cornerRadius=10.0; if(i + (tmpPage*4)<[imagesCatalogList count]){ UIImage* image=[UIImage imageWithContentsOfFile:[NSString stringWithFormat: @"%@/%@",docDir,[imagesCatalogList objectAtIndex:i + (tmpPage*4)]]]; if(image.size.width>image.size.height){ image=[image scaleToSize:CGSizeMake(image.size.width/(image.size.height/200), 200.0)]; CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2,(image.size.height-159.5)/2, 159.5, 159.5)); image = [UIImage imageWithCGImage:ref]; } else if(image.size.width<image.size.height){ image=[image scaleToSize:CGSizeMake(200.0, image.size.height/(image.size.width/200))]; CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2, (image.size.height-159.5)/2, 159.5, 159.5)); image = [UIImage imageWithCGImage:ref]; } else{ image=[image scaleToSize:CGSizeMake(159.5, 159.5)]; } [addButton setBackgroundImage:image forState:UIControlStateNormal]; image=nil; addButton.frame=CGRectMake(width, height, 159.5, 159.5); NSLog(@"width %i height %i", width, height); addButton.tag=i + (tmpPage*4); [addButton addTarget:self action:@selector(modifyImage:) forControlEvents:UIControlEventTouchUpInside]; [tmpScrollView addSubview:addButton]; addButton=nil; photos++; } } } lastPageToAdd=pageToAdd; lastContentOffset=offset.y; } ```