Cocoa in the Shell

Efficiently build a gallery like Photos.app

Quite often people wants to build a gallery like Photos.app for their iOS application. The exercise is really not difficult, but it’s also easy to kill your device memory if you don’t take the time to think of an elegant way to do it.

As I said, the concept is very easy, basically you just need to put some UIImageView inside a UIScrollView and you are done. But several times I saw code where people put as many UIImageView as they had images to display.

Bad idea. Why ?

Let’s say you have 13 images to display of the size of an iPhone screen (320x480).
On iOS 1 pixel = 4bytes.
So in memory, a single picture takes 600Kb. Multiply this by 13 and you are at 7.8Mb used.

And this, wasn’t the worst case scenario. Because often people doesn’t take the time to reduce their pictures to the size of the device and end up displaying, for example, 6 Mega Pixel image (That’s 24Mb in memory for a single image !) or even more, and that’s dramatic.

Good practices

Here the good practices are obvious.

  • First, reduce your pictures size with, for example, ImageIO.
  • Then to build a gallery, 3 UIImageView are enough, really. One for the current displayed image, one for the previous and one for the next.

Knowing this, I’ll show you some simple code to illustrate this.

So first let’s create our view (I don’t use IB)

-(void)loadView
{
    UIView* view = [[UIView alloc] initWithFrame:(CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = kPageWidth, .size.height = 320.0f}];

    UIScrollView* scrollView = [[UIScrollView alloc] initWithFrame:(CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = kPageWidth, .size.height = 320.0f}];
    scrollView.delegate = self;
    scrollView.backgroundColor = [UIColor blackColor];
    scrollView.showsVerticalScrollIndicator = NO;
    scrollView.showsHorizontalScrollIndicator = NO;
    scrollView.alwaysBounceVertical = NO;
    scrollView.alwaysBounceHorizontal = YES;
    scrollView.scrollsToTop = NO;
    scrollView.pagingEnabled = YES;
    scrollView.directionalLockEnabled = YES;
    scrollView.contentSize = (CGSize){.width = kPageWidth * _picturesCount, .height = 320.0f};
    scrollView.contentOffset = (CGPoint){.x = kPageWidth * _index, .y = 0.0f};
    [view addSubview:scrollView];

    _imageView1 = [[UIImageView alloc] initWithFrame:(CGRect){.origin.x = scrollView.contentOffset.x, .origin.y = 0.0f, .size.width = kPageWidth, .size.height = 320.0f}];
    _imageView1.tag = 1;
    [scrollView addSubview:_imageView1];

    _imageView2 = [[UIImageView alloc] initWithFrame:(CGRect){.origin.x = kPageWidth, .origin.y = 0.0f, .size.width = kPageWidth, .size.height = 320.0f}];
    _imageView2.tag = 2;
    [scrollView addSubview:_imageView2];

    _imageView3 = [[UIImageView alloc] initWithFrame:(CGRect){.origin.x = kPageWidth + kPageWidth, .origin.y = 0.0f, .size.width = kPageWidth, .size.height = 320.0f}];
    _imageView3.tag = 3;
    [scrollView addSubview:_imageView3];

    self.view = view;

    [scrollView release];
    [view release];
}
  • kPageWidth has a value of 480.0f.
  • picturesCount, as the name implies, is an integer that represents the number of pictures (ivar).
  • index is the index of the current displayed picture (ivar), starting at 0.

Then we need to implement the scrollViewDidScroll: method.

-(void)scrollViewDidScroll:(UIScrollView*)scrollView
{
    const CGFloat currPos = scrollView.contentOffset.x; // Get current X scrollview position
    const NSInteger selectedPage = lroundf(currPos * (1.0f / kPageWidth)); // Compute selected page
    const NSInteger zone = 1 + (selectedPage % 3); // Current zone : 0 - 1 - 2

    const NSInteger nextPage = selectedPage + 1;
    const NSInteger prevPage = selectedPage - 1;

    /// Next page
    if (nextPage < (NSInteger)_picturesCount)
    {
        NSInteger nextViewTag = zone + 1;
        if (nextViewTag == 4)
            nextViewTag = 1;
        UIImageView* nextView = (UIImageView*)[scrollView viewWithTag:nextViewTag];
        nextView.frame = (CGRect){.origin.x = nextPage * kPageWidth, .origin.y = 0.0f, .size = nextView.frame.size};
        Picture* nextPic = [_pictures objectAtIndex:nextPage];
        UIImage* img = [[UIImage alloc] initWithContentsOfFile:nextPic.Path];
        nextView.image = img;
        [img release];
    }
    /// Prev page
    if (prevPage >= 0)
    {
        NSInteger prevViewTag = zone - 1;
        if (!prevViewTag)
            prevViewTag = 3;
        UIImageView* prevView = (UIImageView*)[scrollView viewWithTag:prevViewTag];
        prevView.frame = (CGRect){.origin.x = prevPage * kPageWidth, .origin.y = 0.0f, .size = prevView.frame.size};
        Picture* prevPic = [_pictures objectAtIndex:prevPage];
        UIImage* img = [[UIImage alloc] initWithContentsOfFile:prevPic.Path];
        prevView.image = img;
        [img release];
    }

    if (_index != selectedPage && selectedPage >= 0 && selectedPage < _picturesCount)
    {
        Picture* currentPic = [_pictures objectAtIndex:selectedPage];
        self.navigationItem.title = currentPic.Title;
        _index = selectedPage;
    }
}
  • Picture is a class to encapsulate an image path, a title for the image and description.
  • I assume your images are already at the good size, you can find how to do this in the link I gave above.

That’s all the mechanisms for the gallery, after that it’s mainly UI customization.