AzoftCase StudiesCreating an Image Gallery for iOS Apps Using UIImageView

Creating an Image Gallery for iOS Apps Using UIImageView

By Sergey Pozhidaev on July 31, 2013

For one of my recent mobile development projects I had to implement a scrollable image gallery for an iOS app. According to the requirements 

  • the gallery should take up as little memory as possible
  • the user should be able to easily scroll left or right
  • should look like the one pictured on the screenshot below.

Note how one image is directly in the center, while the other two are only partly showing. In this post, I’ll describe how I created such an image gallery using UIImageView, without any third-party tools.

ios apps image gallery uiimageview1 Creating an Image Gallery for iOS Apps Using UIImageView

The gallery’s layout is going to be automatically configured and based on the width of the images and spacing between them.

To keep memory usage as low as possible we're not going to render all the images in the scrollView. Instead, we're only going to render the currently visible images and a couple of images to the left and to the right of the screen that will be shown immediately after scrolling the view to either side according to the direction of the gesture. To render the images we'll use the UIImageView class provided by the SDK but it can be replaced by any subclass of UIView. The images will be centered on screen.

Our program will require implementing the following mechanisms:

  • Setting up display components and calculating the offset of the first image.
  • Detecting the offset of the first and last image viewing components while scrolling.
  • Updating the contents of the views according to their position. Aligning the components according to the position of scrollView.

1. Setting up display components and calculating the offset of the first image

First, we need to figure out the number of active components, so that their reuse will be unnoticeable for the end user. Let's assume the minimum number of image viewing components is the ceiling of maximum number of images fitted in the screen added to the spacing and increased by 2.

NSInteger countOfImageViews = ceilf(_scrollView.frame.size.width / (kGalleryImageWidth + kGallerySpaceBetweenImage)) + 2;

Next, let's create an array of size calculated above and fill it with the image viewing components. It's convenient to set some initial properties of the objects and add them to the scrollView at this stage. We'll access them as the members of the array from now on.

    for (int i = 0; i < countOfImageViews; i++) {
        UIImageView *imageView = [[[UIImageView alloc] initWithFrame:CGRectMake(firstImageOffset + (kGallerySpaceBetweenImage + kGalleryImageWidth) * i, 0.0f, kGalleryImageWidth, 30.0f)] autorelease];
        imageView.backgroundColor = [UIColor clearColor];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        
        CALayer *layer = imageView.layer;
        layer.borderColor = [UIColor whiteColor].CGColor;
        layer.borderWidth = imageView.frame.size.width / 20.0;
        
        [_scrollView addSubview:imageView];
        
        [_imagesMutableArray addObject:imageView];
    }

Calculating the offset of the first image is done as a separate method (which will turn out useful in several other places) that uses the following logic: placing the image in the center of the screen and moving it only if the image will look better with an offset at the given width. The autodetected offset is added to a constant called kFirstImageAdditionalOffset, which is equal to 0.0 by default.

- (CGFloat)firstImageOffsetForFrame:(CGRect)frame
{
    NSInteger tempW = floorf((frame.size.width / 2.0f - (kGalleryImageWidth * 3.0f / 2.0f + kGallerySpaceBetweenImage)));
    NSInteger firstImageOffset = 0;
    
    if (tempW < 0) {
        firstImageOffset = floorf((frame.size.width - kGalleryImageWidth) / 2.0f);
    } else {
        NSInteger imageWidthWithSpace = (kGalleryImageWidth + kGallerySpaceBetweenImage);
        firstImageOffset = tempW % imageWidthWithSpace;
    }
    
    return firstImageOffset + kFirstImageAdditionalOffset;
}

2. Detecting the offset of the first and last image viewing components while scrolling

The logic of offset detection for the outermost components is implemented in the UIScrollView delegate method called scrollViewDidScroll:. Our implementation also features detection of the scroll direction which is done by the following code:

    //detect current direction
    if (scrollView.contentOffset.x > _scrollPreviousOffset) {
        _scrollViewScrollDirection = ScrollingDirectionLeft;
    } else if (scrollView.contentOffset.x < _scrollPreviousOffset) {
        _scrollViewScrollDirection = ScrollingDirectionRight;
    }
    
    //increment or rest scrolled length
    _scrollScrolledLength += fabsf(_scrollPreviousOffset - scrollView.contentOffset.x);

    //remember current content offset
    _scrollPreviousOffset = scrollView.contentOffset.x;

    //calculating current images on the left of screen
    NSInteger scrollViewWithoutStartOffset = floorf(scrollView.contentOffset.x - [self firstImageOffsetForFrame:scrollView.frame]);
    NSInteger imageWidthWithSpace = (kGalleryImageWidth + kGallerySpaceBetweenImage);
    
    NSInteger numberOfLeftOffserceenImages = (scrollViewWithoutStartOffset > 0) ? (floorf(scrollViewWithoutStartOffset / imageWidthWithSpace)) : 0;
    
    CGFloat scrollViewRestOffsetPart = scrollViewWithoutStartOffset % imageWidthWithSpace;

    if (scrollViewRestOffsetPart > 0.0 && scrollViewRestOffsetPart >= kGalleryImageWidth) {
        numberOfLeftOffserceenImages +=1;
    }

    //make image movement if allowed and relayout
    BOOL relayoutWasMade = NO;

    BOOL canMoveImageToRight = (scrollView.contentSize.width - scrollView.contentOffset.x - scrollView.frame.size.width) > kGalleryImageWidth;
    if (numberOfLeftOffserceenImages > _previousImagesOnLeft && canMoveImageToRight && numberOfLeftOffserceenImages > 1) {
        UIImageView *firstImageView = [_imagesMutableArray objectAtIndex:0];
        UIImageView *lastImageView = [_imagesMutableArray lastObject];
        
        //remove first view
        [_imagesMutableArray removeObjectAtIndex:0];

        //move first imageto the end of content
        CGRect tempImageViewFrame = firstImageView.frame;
        tempImageViewFrame.origin.x = lastImageView.frame.origin.x + lastImageView.frame.size.width + kGallerySpaceBetweenImage;
        firstImageView.frame = tempImageViewFrame;

        //move first view the end of array
        [_imagesMutableArray addObject:firstImageView];
        
        relayoutWasMade = YES;
    } else if (numberOfLeftOffserceenImages < _previousImagesOnLeft && numberOfLeftOffserceenImages > 0 ) {
        UIImageView *firstImageView = [_imagesMutableArray objectAtIndex:0];
        UIImageView *lastImageView = [_imagesMutableArray lastObject];
        
        [_imagesMutableArray removeLastObject];
        
        CGRect tempImageViewFrame = lastImageView.frame;
        tempImageViewFrame.origin.x = firstImageView.frame.origin.x - lastImageView.frame.size.width - kGallerySpaceBetweenImage;
        lastImageView.frame = tempImageViewFrame;
        
        [_imagesMutableArray insertObject:lastImageView atIndex:0];
        
        relayoutWasMade = YES;
    }

    //if there was relayout we need remember new left images count and configure images
    if (relayoutWasMade) {
        _previousImagesOnLeft = numberOfLeftOffserceenImages;
        [self configureSubviews];
    }

Don't forget to reset the values before the next scrolling procedure.

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    //reset values
    _scrollPreviousOffset = scrollView.contentOffset.x;
    _scrollScrolledLength = 0.0f;
}

3. Updating the contents of the views according to their position

configureSubviews is called at the end of the scrollViewDidScroll: method if the component has been moved. The following code shows what's being done in this method:

- (void)configureSubviews
{
    //adjust imageViews
    for (int i = 0; i < [_imagesMutableArray count]; i ++) {
        UIImageView *imageView = [_imagesMutableArray objectAtIndex:i];
        
        //set image
        NSInteger imageIndex = floorf((imageView.frame.origin.x - [self firstImageOffsetForFrame:_scrollView.frame]) / (kGalleryImageWidth + kGallerySpaceBetweenImage)) + 1;
        
        imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@%i.jpg", kImagePrefix, imageIndex]];
        
        //adjust height and Y origin of imageView
        if (imageView.image) {
            CGRect tempFrame = imageView.frame;
            tempFrame.size.height = kGalleryImageWidth * (imageView.image.size.height / imageView.image.size.width);
            tempFrame.origin.y = floorf((_scrollView.frame.size.height - tempFrame.size.height) / 2.0f);
            imageView.frame = tempFrame;
        }
    }

The component is assigned a number corresponding to the position of the component in scrollView. As you can see from the snipped above, the image is then aligned and scaled to fit the dimensions of other images.

4. Aligning the components according to the position of scrollView

Aligning the components is actually implemented in two delegate methods. If the user releases scrollView while it's idle then scrollViewDidEndDragging is called with the decelerate parameter set to NO and scrollViewWillBeginDecelerating is not being called. In this case image aligning is done only in the scrollViewDidEndDragging method. If scrollView is released while skimming through the images then scrollViewDidEndDragging is called with the decelerate parameter set to YES followed by the call of scrollViewWillBeginDecelerating which contains the actual code to determine whether the image will be switched and it's alignment.

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //if wouldn't decelerate we should adjust contentOffset here
    if (!decelerate) {
        NSInteger leftImageIndex = [self indexOfLeftImageForScrollView:scrollView];
        
        CGPoint contentOffset = [self contentOffsetForLeftImageIndex:leftImageIndex inFrame:scrollView.frame];

        [scrollView setContentOffset:contentOffset animated:YES];
    }
}

The gesture recognition routine determines whether the user intended to perform a gesture to move to the next image or to align the current number of images. The decision is made according to a value of the global variable _scrollScrolledLength. This variable is assigned inside the scrollViewDidScroll method:

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
    NSInteger leftImageIndex = [self indexOfLeftImageForScrollView:scrollView];
    
    //determine if it should be swipe action so we need to increment or decrement number of left images
    if (_scrollScrolledLength < ((kGalleryImageWidth + kGallerySpaceBetweenImage) / 2.0f)) {
        
        switch (_scrollViewScrollDirection) {
            case ScrollingDirectionLeft: {
                BOOL canScrollLeft = (scrollView.contentSize.width - scrollView.contentOffset.x - scrollView.frame.size.width) > 0.0;
                if (canScrollLeft) {
                    leftImageIndex += 1;
                }
            } break;
            case ScrollingDirectionRight: {
                BOOL canScrollRight = (scrollView.contentOffset.x - [self firstImageOffsetForFrame:scrollView.frame]) > 0.0;
                if (canScrollRight) {
                    leftImageIndex -= 1;
                }
            } break;
            case ScrollingDirectionNone: {
            } break;
            default:
                break;
        }
    } 
    
    //set result offset
    CGPoint contentOffset = [self contentOffsetForLeftImageIndex:leftImageIndex inFrame:scrollView.frame];
    
    [scrollView setContentOffset:contentOffset animated:YES];
}

Several helper methods have been used in the code above:

  • indexOfLeftImageForScrollView: returns the index of the image near the left border of the scrollView frame, passed as a parameter. The value of 1 corresponds to an image positioned in the left part of the screen such that it's at least half visible to the user.
  • contentOffsetForLeftImageIndex:inFrame: returns the offset of the view contents calculated from the image index defined above and the dimensions of currently visible part of the image.

The gallery can now be configured by adjusting the kGalleryImageWidth and kGallerySpaceBetweenImage constants:

- (CGPoint)contentOffsetForLeftImageIndex:(NSInteger)leftImageIndex inFrame:(CGRect)frame
{
    //if there are no images beyond left corner return just zero point;
    if (leftImageIndex <= 0) {
        return CGPointZero;
    }
    
    //calculating offset that fits requirements image must be in center
    //offset of firstImage
    CGFloat offsetPart1 = [self firstImageOffsetForFrame:frame];
    
    //adjusting part
    NSInteger screenLeftPartFromCenterImage = floorf((frame.size.width - kGalleryImageWidth) / 2.0);
    NSInteger imageWidthWithSpace = (kGalleryImageWidth + kGallerySpaceBetweenImage);

    CGFloat restOnScreenPart = screenLeftPartFromCenterImage % imageWidthWithSpace;
    CGFloat imagesWithSpacesSize = imageWidthWithSpace * leftImageIndex;
    
    CGFloat offsetPart2 = imagesWithSpacesSize - restOnScreenPart;
    
    //make need offset or to 0.0 if there are no images on the left
    CGPoint contentOffset = CGPointMake(offsetPart1 + offsetPart2, 0.0f );
    
    return contentOffset;
}

- (NSInteger)indexOfLeftImageForScrollView:(UIScrollView *)scrollView
{
    NSInteger contentOffsetWithoutStartOffset = floorf(scrollView.contentOffset.x - [self firstImageOffsetForFrame:scrollView.frame]);
    NSInteger imageWidthWithSpace = kGallerySpaceBetweenImage + kGalleryImageWidth;
    
    NSInteger indexOfLeftScreenImage = floorf(contentOffsetWithoutStartOffset / imageWidthWithSpace);
    
    CGFloat restPart = contentOffsetWithoutStartOffset % imageWidthWithSpace;
    
    if (restPart > kGalleryImageWidth / 2.0) {
        indexOfLeftScreenImage += 1;
    }
    
    return indexOfLeftScreenImage;
}

Now changing kGalleryImageWidth and kGallerySpaceBetweenImage we can adjust the gallery:

ios apps image gallery uiimageview 03 Creating an Image Gallery for iOS Apps Using UIImageView ios apps image gallery uiimageview 04 Creating an Image Gallery for iOS Apps Using UIImageView ios apps image gallery uiimageview 05 Creating an Image Gallery for iOS Apps Using UIImageView

ios apps image gallery uiimageview 06 Creating an Image Gallery for iOS Apps Using UIImageView ios apps image gallery uiimageview 07 Creating an Image Gallery for iOS Apps Using UIImageView ios apps image gallery uiimageview 08 Creating an Image Gallery for iOS Apps Using UIImageView

Conclusion

You might be thinking, why go through all these steps when we can just use some third-party class or plug-in, such as iCarousel? Of course, there are many third-party tools out there, which could potentially save developers tons of time. However, third-party libraries also have many disadvantages. In our case, using iCarousel was not an option because it takes up lots of memory and can’t be easily customized. That’s why we decided the best approach would be to create our own image-scrolling gallery, exactly as the client’s requirement stated.

VN:F [1.9.22_1171]
Rating: 5.0/5 (7 votes cast)
VN:F [1.9.22_1171]
Rating: +7 (from 9 votes)
Creating an Image Gallery for iOS Apps Using UIImageView, 5.0 out of 5 based on 7 ratings

Content created by Sergey Pozhidaev