Basics
To enable scrolling your content in a
UIScrollView, set itscontentSize, which tells theUIScrollViewhow much content there is.To know what portion of the content is currently shown on screen, use
UIScrollView’scontentOffset, which represents the top left current visible point on the scroll view frame (a.k.a. The point at which the origin of the content view is offset from the origin of the scroll view).To get zooming working on a scroll view:
create a type conforming to
UIScrollViewDelegate, implementviewForZooming(in:)set an instance of this type as your
UIScrollViewinstancedelegateset the
minimumZoomScaleand themaximumZoomScalein yourUIScrollViewinstance to be different (both are1.0by default)
Advanced Techniques
Infinite scrolling
Stationary views
Custom touch handling
Redraw after zooming
1. Infinite scrolling
The user can keep scrolling in one direction and never hit the edge of the content (e.g. a photo carousel that automatically wraps).
How to achieve this:
make the
contentSizeabout twice the size of what’s visible on screenwhen the user is about to hit the content edge, adjust the
contentOffsetto go into the middle of thecontentSize(a.k.a the scrollable area)adjust the frames of our content subviews to the same amount as the
contentOffsetso that they’re still centered in the visible content area
The last two steps needs to be done concurrently and the user won’t be able to notice.
Where to implement this: the idea is to re-layout those subviews every time the user scrolls.
We have two possible ways:
sub-class
UIScrollView, and override thelayoutSubviews()method (the WWDC session uses this one).layoutSubviews()is called at every frame of zooming and scrolling (a.k.a. anytime the scroll view bounds change)use
UIScrollViewDelegate’sscrollViewDidScroll(_:)
In layoutSubviews() we will:
call
UIScrollView’ssetContentOffset(_:animated:), to shift the content backset
UIView’scenterofframe, to shift subviews by the same amount as the scroll view content
Code for infinite horizontal scroll view:
@implementation InfiniteScrollView
// Recenter content periodically to achieve impression of infinite scrolling
- (void)recenterIfNecessary
{
CGPoint currentOffset = [self contentOffset];
CGFloat contentWidth = [self contentSize].width;
CGFloat centerOffsetX = (contentWidth - [self bounds].size.width) / 2.0;
CGFloat distanceFromCenter = fabs(currentOffset.x - centerOffsetX);
// We re-center when the offset is greater than 25% off the center, this is arbitrary.
if (distanceFromCenter > (contentWidth / 4.0)) {
self.contentOffset = CGPointMake(centerOffsetX, currentOffset.y);
// Move content by the same amount so it appears to stay still
for (UILabel *label in self.visibleLabels) {
CGPoint center = [self.labelContainerView convertPoint:label.center toView:self];
center.x += (centerOffsetX - currentOffset.x);
label.center = [self convertPoint:center toView:self.labelContainerView];
}
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self recenterIfNecessary];
}
...
@end2. Stationary views
Views that remain pinned in place in one dimension/direction, but scroll with the scrolling content on the other axis. Think like headers and footers.
This can be the case where we have one piece of the content that should not zoom or scroll along with the rest of the content (e.g. the title of an image).
In the session they implement a scenario where:
there’s an image title that sticks on top of the scroll view
the image can be scrolled and zoomed, the title stays in place
the title only disappears when the user scrolls down on the image, so that the image can be seen in full
any other interaction (zoom or scroll will make the title reappear)
Configuration:
We have one scroll view that takes the whole available space, which has two subviews:
the header/title view, which doesn’t zoom
the
UIImageViewthat can be zoomed, this is the view returned inviewForZooming(in:)
What to do next:
Since only our
UIImageViewcan scroll, the first thing we need to do is to make sure that our header view stays centered horizontally when we zoom/scroll in the image view. This is done by setting the headerframe.origin.xto be equivalent tocontentOffset.xinlayoutSubviews().when we zoom in a scroll view, the scroll view content size is automatically updated to the zoomed size of the view returned in
viewForZooming(in:). In our case, we also need the scroll view to consider the header view size, hence we need to override thecontentSizesetter to also consider the header.
3. Custom touch handling
The session focuses on adding multi-touch handlers to subviews of the scroll view.
You can get the UIScrollView’s pan and pitch gesture recognizers via the panGestureRecognizer and pinchGestureRecognizer properties. These are the same recognizers tat UIScrollView uses to manage its own gestures (for scrolling and zooming, respectively).
In this session they implement a scenario where:
swiping up/down from the bottom of the scroll view will make another view appear/disappear instead of scrolling in the scroll view
Implementation (you can add this code in loadView()/viewDidLoad():
UIScrollView *scrollView = [self scrollView]:
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action: @selector (handleSwipeUp:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
[scrollView addGestureRecognizer: swipeUp];
// 👇🏻 this is required, otherwise the scrollView.panGestureRecognizer would always trigger before our swipe up
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:swipeUp];Note that this implementation makes the scroll view pan gesture to wait to trigger because it needs to make sure the gesture is not a swipe. However, in our case we want this behavior only for the bottom of the scroll view and not the whole screen. So we can limit the target area:
- (BOOL) gestureRecognizer: (UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch: (UITouch *) touch
{
UIScrollView #scrollView = [self scrollView];
CGRect visibleBounds = [scrollView bounds]:
CGPoint touchPoint = [touch locationInView:scrollView];
if (touchPoint.y < CGRectGetMaxY(visibleBounds) - 75)
return NO:
return YES;
}4. Redraw after zooming
📚 Download
ScrollViewSuitecode sample. DownloadPhotoScrollercode sample.
The session focuses on small bits of content that need to be redrawn once the user is done zooming.
The idea is that we content gets zoomed in, it starts to get blurry, so you want to redraw it to make it crisp once again.
We do this redraw only after the zoom has ended, not while the user is zooming (the reason being this operation is expensive and we’d discard things constantly as the user keeps zooming): this can be obtained via scrollViewDidEndZooming(_:with:atScale:), which also lets us know what is the final scale.
How-to (⚠️ only for small pieces of content):
- (void)scrollViewDidEndZooming: (UIScrollView *) sv
withView: (UIView *) view
atScale: (float)scale
{
scale *= [[[scrollView window] screen] scale]:
[view setContentScaleFactor:scale]:
}The contentScaleFactor of a view is essentially a multiplier applied to the bound size of the view used to determine how big your view rect backing storage should be.
