UIScrollView basics

UIScrollView is one of the most commonly used classes in UIKit, either directly or indirectly (by using one of its subclasses – UITableView, UICollectionView or UITextView).

A scroll view is used when the content to be presented on the screen is larger than the size of the view (represented by its bounds) in one or both directions (vertically or horizontally). As the user drags inside the scroll view, the content goes in and out the visible area.

Frame and bounds overview

Before discussing how the scroll view works, a little overview of the coordinate system in iOS.

A view (instance or subclass of UIView) is the rectangular area where content is drawn. Each view has its own 2D coordinate system which extends infinitely on the horizontal and vertical axis (or as much as the maximum CGFloat value allows). The origin of the view is the top-left corner of the view’s rectangle. By default, it’s equal to the coordinate system origin, the point at the {0, 0} position. As we’ll see later, the view’s origin plays a very important role in scrolling.

The view origin and its size (width on the X axis and height on the Y axis) represent its bounds.

The subviews are placed in the view’s coordinate system. In addition to their bounds, the subviews also have a frame representing their position relative to the superview. The frame’s origin is the point in the superview coordinate system where the top-left corner of the subview is located.

The subview’s origin can be inside (subview A) or outside (subviews B and C) the visible area of the super-view. The frame size is usually identical to its bounds size, but it could be different if we apply a transformation to the subview (using Core Animation). As we can see in the next diagram, subview A frame and bounds have different sizes because it was rotated:

view coordinate system

How does the UIScrollView scroll ?

As for any other type of view, the UIScrollView position in the superview coordinate system is defined by its frame.

When a user scrolls inside an UIScrollView, its frame doesn’t change, its bounds size doesn’t change, but the bounds origin shifts in the opposite direction to the pan gesture, revealing the neighboring area of the view’s content which was clipped (hidden) by the scroll view’s bounds. As explained previously, the bounds are related to the view’s coordinate system, so the bounds origin moves inside the scroll view’s space.

By default, the content area’s origin is the same as the scroll view’s bounds origin, and it’s the {0, 0} point. When the user scrolls, the bounds origin varies between {0, 0} and {contentSize.width – scrollView.bounds.size.width, contentSize.height – scrollView.bounds.size.height}, allowing the content to be scrolled from its top-left to its bottom-right corner.

For the bounds origin to change position, we have to set the scroll view’s contentSize property (specifying its width and height). By default, the contentSize is {0, 0} and adding subviews to the scroll view doesn’t change it. If we’re using Auto Layout, the contentSize is computed dynamically (as long as all constraints are satisfied and the system is able to figure out the width and height of the content area).

Automatic scrolling in code

The contentOffset property of the scroll view represents the current position of the bounds origin as a CGPoint. We can set this property in code and the content is automatically scrolled to that position. Further more, the action can be animated if we use the method setContentOffset:animated:.

Another way to scroll automatically to a portion of the content is by invoking the scrollRectToVisible:animated: method. The first argument is a CGRect which corresponds to the area of the content to be visible in the scroll view. The CGRect is relevant to the scroll view coordinate system.

Scrolling behind the bars

Starting with iOS7, it’s common practice to have translucent status and navigation bars, showing a blurred image of the content behind them.

If the scroll view is full screen (which is usually the case), the content behind the bars is not visible unless we set the contentInset property, which add margins to the container area allowing the actual content to be scrolled until it’s entirely visible. For example, if we have status and navigation bars, the contentInset property should be UIEdgeInsetsMake(64, 0, 0, 0).

Remember to also set the scrollIndicatorInsets property to the same value, otherwise the scroll indicators will overlap the bars.

For full screen scroll views set in Interface Builder as the only subview of a UIViewController main view, the automaticallyAdjustsScrollViewInsets property of the view controller takes care of setting the contentInset and scrollIndicatorInsets (default value YES).

Loading large contents

Scroll views are sometimes used to display very large amount of data (high-resolution images, maps, etc). Loading all the data at once and then displaying it is time and memory consuming, and it might event be impossible.

In this kind of situation, we have to split the content in smaller chunks, then load and display them “on demand”.

For example, if we had to show a large image in an UIScrollView, we would first split it in several small images of same size. Then we would add a UIView as a subview of the scroll view setting its frame size to the size of the large image. We would then use a CATiledLayer object as the backing layer of subview; when the subview is scrolled, the system calls the -drawRect: method of the CATiledLayer and we would load in this method the small image corresponding to the visible area.

For further information, check out the CATiledLayer sample project provided by Apple.

Conclusion

Apps that don’t make use of UIScrollView in one form or another are very rare. I recommend reading this article from objc.io online magazine to get a better understanding of its mechanics.

 

Catalin Rosioru