iOS 7 introduced a layered design language where the system achieved to create the illusion of three dimensionality by moving views on screen with the movement of the device. The parallax on the lock screen is probably the best known example of this effect. Some system views such as alert dialogs use this effect and as a developer you can use it in your own apps with the UIMotionEffect classes. Ash Furrow wrote a really nice tutorial on how to accomplish this back then in 2013.

Another place where a similar parallax effect can be quite useful is scroll views where the contents of the cells scroll slightly differently than the main scroll view to give the user the illusion of as if the cells are cut-outs showing an object which sits deeper into the screen. The probably best known example of this effect is images in WhatsApp chats. Below you can see this effect in action in a simple demo app:

A collection view containing image cells with parallax scrolling

I recently wanted to implement something similar for a collection view with a custom layout. The most straight forward way to achieve this effect is to implement scrollViewDidScroll: and adjust the contents of the cells accordingly as in this project. Another, more elegant approach is like Ole Begemann described in his blog post as part of a UICollectionViewLayout.

Since I was already using a custom quilt layout, I decided to subclass and extend it with parallax scrolling as Ole described.

Which worked great, as expected, but only in the simulator…

Since the layout gets invalidated at each scroll event, it needs to be recomputed constantly. And in the case of the quilt layout this turned out to be very expensive: a collection with about 50 photos could not be scrolled at 60fps on an iPhone 6.

This could be resolved by implementing some smart caching of the layout attributes in the layout subclass and returning them only by adjusting their parallax offsets. Depending on the nature of the layout this could introduce significant complexity. Complexity is usually a pretty good sign of our code trying to tell us that we’re swimming against the flow.

So, I decided to listen to my code and implement parallax scrolling by implementing scrollViewDidScroll: and without touching the layout attributes. Since the app I’m developing, has multiple collection views full of photos already implemented, I could either introduce a common superclass and implement scrollViewDidScroll: there or go the composition route. Due to its flexibility I chose the composition route and decided to implement parallax scrolling as a category on UICollectionViewController

Here is the header for the ParallaxScroll category:

//UICollectionViewController+ParallaxScroll.h
@import UIKit;

@protocol UICollectionViewCellParallax <NSObject>

- (void)updateWithParallaxOffset:(CGPoint)offset;

@end

@interface UICollectionViewController (ParallaxScroll)

@end

It declares an informal protocol with a single method updateWithParallaxOffset:, which we will later implement in our collection view cells which should have parallax scroll.

And the implementation contains a single method:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSArray *visibleCells = self.collectionView.visibleCells;
    
    for (UICollectionViewCell *cell in visibleCells) {
        if ([cell respondsToSelector:@selector(updateWithParallaxOffset:)]) {
            CGRect bounds = self.collectionView.bounds;
            CGPoint boundsCenter = CGPointMake(CGRectGetMidX(bounds),
                                               CGRectGetMidY(bounds));
            CGPoint cellCenter = cell.center;
            CGPoint offsetFromCenter = 
                CGPointMake(boundsCenter.x - cellCenter.x,
                            boundsCenter.y - cellCenter.y);
            
            CGSize cellSize = cell.bounds.size;
            CGFloat maxVerticalOffset =
            (bounds.size.height / 2) + (cellSize.height / 2);
            CGFloat scaleFactor = 30. / maxVerticalOffset;
            
            CGPoint parallaxOffset = CGPointMake(0.0, -offsetFromCenter.y * scaleFactor);
            [(id)cell updateWithParallaxOffset:parallaxOffset];
        }
    }
}

Since the scrollViewDidScroll: is implemented at the UICollectionViewController level all of its subclasses in our app are going to be able to handle parallax scroll. On the other hand, we can implement scrollViewDidScroll: in the subclasses and still retain the parallax scrolling as long as the child implementations call [super scrollViewDidScroll:]. The informal protocol UICollectionViewCellParallax is central to how this approach works. Parallax scrolling can be selectively turned on and off by implementing the updateWithParallaxOffset: or not in a UICollectionViewCell subclass. For a simple photo cell, updateWithParallaxOffset: can be as simple as moving the image view accordingly:

- (void)updateWithParallaxOffset:(CGPoint)offset {
    self.imageViewCenterYConstraint.constant = offset.y;
}

By implementing parallax scroll with a category and an informal protocol, we get the best of both approaches: the scrolling stays very smooth because the layout does not get recomputed at every scroll position, which might be expensive, and we do get to cleanly separate parallax scroll from the rest of our view controller code in its separate location.