UIScrollView and Memory Allocations

I ran into a curious problem while using nib-based UIView hierarchies as part of a horizontally scrolling UIScrollView. I followed the Apple PageControl example of progressively loading the next and last page’s viewcontrollers and adding them as subviews, and I also took the step of progressively unloading the viewcontrollers (see below) outside those bounds. Still, every time I was paging back and forth, my real memory allocations would increase by a few MB for every page!

This became a problem pretty quickly, as you can imagine. I ended up finding a solution when I saw this post about the reverse issue.

It turns out that if you assign a viewcontroller’s view as a subview of a UIScrollView, you must both send a removeFromSuperview message to the subview, and also replace the viewcontroller’s spot in the NSArray container in order to get it released.

- (void)unloadPage:(int)page {
if (page < 0) return;
if (page >= kNumItems) return;
if ((NSNull *)[viewControllers objectAtIndex:page] != [NSNull null]) {
ItemViewController *controllerToDelete =
                    [viewControllers objectAtIndex:page];
// Release the view from the scrollview view
[controllerToDelete.view removeFromSuperview];
// release the viewcontroller from the collection
[viewControllers replaceObjectAtIndex:page
                    withObject:[NSNull null]];
}
}

This ended up making my memory allocations steady, but it didn’t totally get rid of the hitching that was present. I believe that because my UIViews held in the UIScrollView load images from the Documents folder, I/O was getting triggered every time a pagination boundary was crossed, which in the PageControl example is unfortunately right in the middle of the transition. However, a smarter way to do the loading is to make most loads occur within the scrollViewDidEndDecelerating: event handler, like so:

- (void) loadSurroundingPagesForPage:(int)page {

[self unloadPage:page + kSurroundingPagesToLoad + 1];
[self unloadPage:page - kSurroundingPagesToLoad - 1];

for (int i=1; i < kSurroundingPagesToLoad; i++) {
[self loadPage:page + i];
[self loadPage:page - i];
}

[self loadPage:page];

lastPageLoaded = page;

}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    pageControlUsed = NO;
[self loadSurroundingPagesForPage:pageControl.currentPage];
}

That way, in normal use, a user flipping slowly through the scrolled items won’t notice the hitch because the subview will be still while it happens! Unfortunately, this means that a user who is continuously scrolling might scroll completely past the loaded content, leading to a “blank” – so, if you’re willing to take a hitch, you can force a load in those conditions.

- (void)scrollViewDidScroll:(UIScrollView *)sender {
if (pageControlUsed) {
return;
}

CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth/2) / pageWidth) + 1;
pageControl.currentPage = page;
// Change of tactics - do lazy load in between pages IFF the new page requested is near our bounds
if (abs(lastPageLoaded - page) >= kSurroundingPagesToLoad-1) {
NSLog(@"Reached bounds without decelerating - must do a lazy load");
[self loadSurroundingPagesForPage:pageControl.currentPage];
}

}

This last bit takes the hitch in the middle of the page transition if we run out of our buffer zone of UIScrollView subviews. A couple possible optimizations come to mind – well, first, you could up the value of the kSurroundingPagesToLoad constant. Also, you could take account of the direction of the scrolling, and load more pages in the forward scroll direction than in the reverse. It would sort of be like a dual-clutch transmission at that point, only unable to cope when shifting unexpectedly or too many times in a row.

Anyway, this approach seems to work well for my project, and it uses the fetchedresultcontroller as well to cache and lazily load the coredata objects which populate the child views, so all of the scrollview stuff only takes up 4-5 MB of data while running. Pretty nice if you ask me!

I hope that helps anyone who was in the same boat as me. As always, use any of the code you find here at your own risk! Good luck! πŸ™‚

14 thoughts on “UIScrollView and Memory Allocations

  1. Hi,

    Thanks for the post, I’m having the exact same issues. Would you mind specifying where you call “unloadPage” from, and what logic you’re using to determine wich pages are out of bounds? I tried doing it like this, but ended up getting some “you sent a message to a deallocated object” errors..

    (inside scrollViewDidScroll)

    [self loadScrollViewWithPage:page – 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];

    [self unloadScrollViewWithPage:page -2];
    [self unloadScrollViewWithPage:page +2];

  2. @Mike, judging from the selectors in your comment, it looks like you might be confused. You’re not loading scrollviews, you’re loading pages within the scrollviews. In my post, I discuss unloading pages when deceleration ends as well as when scrolling takes us past the boundary of loaded pages. This eliminates hitching during normal use, but doesn’t try to avoid it if we’re scrolling in a direction faster than we can read the content.

  3. This is exactly what I was looking for. One hitch – I do not have an ItemViewController in my use of the PageControl example. I just need to remove from the existing viewcontrollers array in the original.What do you suggest?

  4. Putting the following in the scrollViewWillBeginDragging instead of scrollViewDidEndDecelerating seems to work better for me, but I am not sure if that is correct:

    [self loadSurroundingPagesForPage:pageControl.currentPage];

  5. @Vibhor: That’s interesting, are you seeing a hitch when you scroll one page at a time, or if you scroll quickly? As I mentioned in the post, this technique is meant for interfaces where the user flips pages one at a time. If you load pages on the beginning of scroll dragging, you might end up with a little bit of lag on the beginning, but it will also end up running out of pages one page earlier than if you do it at the end of decelerating.

  6. good tutorial thx, just have 1 question regards of the code below

    [controllerToDelete.view removeFromSuperview];

    [viewControllers replaceObjectAtIndex:page withObject:[NSNull null]];

    i Manage to release it from the superview, but when it execute the replace object with NSNull, it crash my application, im still trying to find out what is the issue.

    thx in advance and look forward for ur comment

  7. Hi, does anyone have a sample project for this??

    it would be really really appreciated!!

    thanks

  8. Hi! Me again πŸ™‚

    Why not just do this?

    – (void)scrollViewDidScroll:(UIScrollView *)sender {
    if (pageControlUsed) {
    return;
    }
    CGFloat pageWidth = scrollView.frame.size.width;
    int page = floor((scrollView.contentOffset.x – pageWidth / 2) / pageWidth) + 1;
    pageControl.currentPage = page;

    for (int i = 0; i < [viewControllers count]; i++) {
    if (i (page + 1)) {
    [self unloadPage:i];
    } else {
    [self loadScrollViewWithPage:i];
    }
    }
    }

  9. Hi Dragan,

    It’s been a while since I looked at this code, and it looks like WP comments mangled your code post a bit. Are you sure that won’t trigger in the middle of scrolling, causing a hitch? If you don’t have a viewcontroller that hitches on load (due to say, loading an image from coredata, or the fs), then you may not need to worry about this at all. πŸ™‚

    -Gordon

  10. @Evan, you have some sort of view controller, yes? So what you need to do is still the same – remove it from the superview and also remove it from the viewControllers array.

  11. Hey Can you post your project here as it would really help me out in figuring out the pagecontrol unloading?

    I have a big project to implement based on it

  12. TQ for sharing this.. its great but i still can’t follow seem there was too much new variable you added and they are not in the apple’s pagecontrol example..

Comments are closed.