iPhone/iPad – AppStore like UIScrollView with paging and preview

Do you like the way application images are displayed in AppStore or the way Safari flips between tabs? This post contains a complete  horizontal, paged UIScrollView with preview control.

Alexander Repty posted a sample on his blog and my sample is based on his work. I’ve encapsulated the solution into one, easy to use, control and solved the problem where “pages” in the scrollview didn’t detect touch/tap events. Here’s a short video showcasing the control:


UIScrollView – preview

The control (BSPreviewScrollView) subclass UIView and contains a standard UIScrollView. The trick to achieve the preview experience is to disable clipping on the UIScrollView by setting “clipsToBounds” to NO. This makes content positioned outside the actual frame bounds to be visible. However, the content that would otherwise be hidden doesn’t respond to touch event such as scrolling. This is solved by listening to the “hitTest” event in BSPreviewScrollView and returning the UIScrollView if the touch is inside the preview areas.

There’s a difference here from Alexander Reptys solution where the UIScrollView was always returned in the hitTest method. This caused touch events from reaching the views inside the UIScrollView. Instead, BSPreviewScrollView defaults to base functionality unless the touch is actually outside the UIScrollView, ie, in the preview areas. This way the UIScrollView functions as expected and no functionality is crippled because of the preview function.

Preview padding

It’s important to notice that if you want padding or space between your views, which i suspect you do, you must handle this within the views you are adding to BSPreviewScrollView. In the sample app attached to this post I place UIImageViews inside the scroll view and while the actual view size is 240×320 pixels the image is just 210×280, this makes for 15 pixels of empty space on each side of the image. When lining two views next to each other this totals in 30 pixels of empty space between the images. So the content inside the views you want to display should be horizontally centered with empty pixels on both sides.

UIScrollView – Paging

Paging is taken care of by the standard paging functionality in UIScrollView.


BSPreviewScrollView code

BSPreviewScrollView.h

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
 
 
@class BSPreviewScrollView;
 
@protocol BSPreviewScrollViewDelegate
@required
-(UIView*)viewForItemAtIndex:(BSPreviewScrollView*)scrollView index:(int)index;
-(int)itemCount:(BSPreviewScrollView*)scrollView;
 
@end
 
 
@interface BSPreviewScrollView : UIView<UIScrollViewDelegate> {
	UIScrollView *scrollView;	
	id<BSPreviewScrollViewDelegate, NSObject> delegate;
	NSMutableArray *scrollViewPages;
	BOOL firstLayout;
	CGSize pageSize;
	BOOL dropShadow;
}
@property (nonatomic, retain) UIScrollView *scrollView;
@property (nonatomic, assign) id<BSPreviewScrollViewDelegate, NSObject> delegate;
@property (nonatomic, assign) CGSize pageSize;
@property (nonatomic, assign) BOOL dropShadow;
 
- (void)didReceiveMemoryWarning;
- (id)initWithFrameAndPageSize:(CGRect)frame pageSize:(CGSize)size;
 
@end

BSPreviewScrollView.m

#import "BSPreviewScrollView.h"
 
#define SHADOW_HEIGHT 20.0
#define SHADOW_INVERSE_HEIGHT 10.0
#define SHADOW_RATIO (SHADOW_INVERSE_HEIGHT / SHADOW_HEIGHT)
 
@implementation BSPreviewScrollView
@synthesize scrollView, pageSize, dropShadow, delegate;
 
 
- (void)awakeFromNib
{
	firstLayout = YES;
	dropShadow = YES;
}
 
- (id)initWithFrame:(CGRect)frame
{
	if(self = [super initWithFrame:frame])
	{
		firstLayout = YES;
		dropShadow = YES;
	}
 
	return self;
}
 
- (id)initWithFrameAndPageSize:(CGRect)frame pageSize:(CGSize)size 
{    
	if (self = [self initWithFrame:frame]) 
	{
		self.pageSize = size;
    }
    return self;
}
 
-(void)loadPage:(int)page
{
	// Sanity checks
    if (page < 0) return;
    if (page >= [scrollViewPages count]) return;
 
	// Check if the page is already loaded
	UIView *view = [scrollViewPages objectAtIndex:page];
 
	// if the view is null we request the view from our delegate
	if ((NSNull *)view == [NSNull null]) 
	{
		view = [delegate viewForItemAtIndex:self index:page];
		[scrollViewPages replaceObjectAtIndex:page withObject:view];
	}
 
	// add the controller's view to the scroll view	if it's not already added
	if (view.superview == nil) 
	{
		// Position the view in our scrollview
		CGRect viewFrame = view.frame;
		viewFrame.origin.x = viewFrame.size.width * page;
		viewFrame.origin.y = 0;
 
		view.frame = viewFrame;
 
		[self.scrollView addSubview:view];
	}
}
 
// Shadow code from http://cocoawithlove.com/2009/08/adding-shadow-effects-to-uitableview.html
- (CAGradientLayer *)shadowAsInverse:(BOOL)inverse
{
    CAGradientLayer *newShadow = [[[CAGradientLayer alloc] init] autorelease];
    CGRect newShadowFrame =	CGRectMake(0, 0, self.frame.size.width, inverse ? SHADOW_INVERSE_HEIGHT : SHADOW_HEIGHT);
    newShadow.frame = newShadowFrame;
    CGColorRef darkColor =[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:inverse ? (SHADOW_INVERSE_HEIGHT / SHADOW_HEIGHT) * 0.5 : 0.5].CGColor;
    CGColorRef lightColor =	[self.backgroundColor colorWithAlphaComponent:0.0].CGColor;
    newShadow.colors = [NSArray arrayWithObjects: (id)(inverse ? lightColor : darkColor), (id)(inverse ? darkColor : lightColor), nil];
    return newShadow;
}
 
- (void)layoutSubviews
{
	// We need to do some setup once the view is visible. This will only be done once.
	if(firstLayout)
	{
		// Add drop shadow to add that 3d effect
		if(dropShadow)
		{
			CAGradientLayer *topShadowLayer = [self shadowAsInverse:NO];
			CAGradientLayer *bottomShadowLayer = [self shadowAsInverse:YES];
			[self.layer insertSublayer:topShadowLayer atIndex:0];
			[self.layer insertSublayer:bottomShadowLayer atIndex:0];
 
			[CATransaction begin];
			[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
 
			// Position and stretch the shadow layers to fit
			CGRect topShadowLayerFrame = topShadowLayer.frame;
			topShadowLayerFrame.size.width = self.frame.size.width;
			topShadowLayerFrame.origin.y = 0;
			topShadowLayer.frame = topShadowLayerFrame;
 
			CGRect bottomShadowLayerFrame = bottomShadowLayer.frame;
			bottomShadowLayerFrame.size.width = self.frame.size.width;
			bottomShadowLayerFrame.origin.y = self.frame.size.height - bottomShadowLayer.frame.size.height;
			bottomShadowLayer.frame = bottomShadowLayerFrame;
 
			[CATransaction commit];
		}
 
		// Position and size the scrollview. It will be centered in the view.
		CGRect scrollViewRect = CGRectMake(0, 0, pageSize.width, pageSize.height);
		scrollViewRect.origin.x = ((self.frame.size.width - pageSize.width) / 2);
		scrollViewRect.origin.y = ((self.frame.size.height - pageSize.height) / 2);
 
		scrollView = [[UIScrollView alloc] initWithFrame:scrollViewRect];
		scrollView.clipsToBounds = NO; // Important, this creates the "preview"
		scrollView.pagingEnabled = YES;
		scrollView.showsHorizontalScrollIndicator = NO;
		scrollView.showsVerticalScrollIndicator = NO;
		scrollView.delegate = self;
 
		[self addSubview:scrollView];
 
 
		int pageCount = [delegate itemCount:self];
		scrollViewPages = [[NSMutableArray alloc] initWithCapacity:pageCount];
 
		// Fill our pages collection with empty placeholders
		for(int i = 0; i < pageCount; i++)
		{
			[scrollViewPages addObject:[NSNull null]];
		}
 
		// Calculate the size of all combined views that we are scrolling through 
		self.scrollView.contentSize = CGSizeMake([delegate itemCount:self] * self.scrollView.frame.size.width, scrollView.frame.size.height);
 
		// Load the first two pages
		[self loadPage:0];
		[self loadPage:1];
 
		firstLayout = NO;
	}
}
 
 
 
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
 
	// If the point is not inside the scrollview, ie, in the preview areas we need to return
	// the scrollview here for interaction to work
	if (!CGRectContainsPoint(scrollView.frame, point)) {
		return self.scrollView;
	}
 
	// If the point is inside the scrollview there's no reason to mess with the event.
	// This allows interaction to be handled by the active subview just like any scrollview
	return [super hitTest:point	withEvent:event];
}
 
-(int)currentPage
{
	// Calculate which page is visible 
	CGFloat pageWidth = scrollView.frame.size.width;
	int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
 
	return page;
}
 
#pragma mark -
#pragma mark UIScrollViewDelegate methods
 
-(void)scrollViewDidScroll:(UIScrollView *)sv
{
	int page = [self currentPage];
 
 
	// Load the visible and neighbouring pages 
	[self loadPage:page-1];
	[self loadPage:page];
	[self loadPage:page+1];
}
 
#pragma mark -
#pragma mark Memory management
 
// didReceiveMemoryWarning is not called automatically for views, 
// make sure you call it from your view controller
- (void)didReceiveMemoryWarning 
{
	// Calculate the current page in scroll view
    int currentPage = [self currentPage];
 
	// unload the pages which are no longer visible
	for (int i = 0; i < [scrollViewPages count]; i++) 
	{
		UIView *viewController = [scrollViewPages objectAtIndex:i];
        if((NSNull *)viewController != [NSNull null])
		{
			if(i < currentPage-1 || i > currentPage+1)
			{
				[viewController removeFromSuperview];
				[scrollViewPages replaceObjectAtIndex:i withObject:[NSNull null]];
			}
		}
	}
 
}
 
- (void)dealloc 
{
	[scrollViewPages release];
	[scrollView release];
	[super dealloc];
}
 
 
@end

Download sample application

Download the sample application utilizing BSPreviewScrollView

35 thoughts on “iPhone/iPad – AppStore like UIScrollView with paging and preview

  1. Hi!I also want zooming+Paging+UIScrollview all in One and i get images from any specific Url so do you have any idea about it.

  2. this code sucks. one unmentioned point is that the images used are all the same width, at exactly 30px less than the width of the scroll view, thus giving it the illusion of padding. that’s not very useful if you don’t have control over the images!

  3. @asdf, there are multiple ways to take control over images. If you care to think, there is image resizing functionality in the SDK.

    Here’s an example of how to do it.

    + (UIImage*)imageWithImage:(UIImage*)image
    scaledToSize:(CGSize)newSize;
    {
    UIGraphicsBeginImageContext( newSize );
    [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return newImage;
    }

    FYI, i think ignorant people suck.

  4. Hi Bjorn,

    Thank you for posting this great tutorial and sample code. It’s been really helpful for me getting my head round gestures and UIscrollviews.

    I’m still struggling with my app because I want the double tap on the scrollview to load a previous view but I can’t get it to work.

    All the best,

    kev.

  5. Hi Bjorn. Best Regards. I try to write a Magazine-App with nested Scrollviews with 14 Pages horizontally and from each of these Pages maybe 15 Pages vertically. Additionally each Page has a different Layout in Landscape/Portrait.
    I did subclass your BSPreviewScrollView to a MainSV and a PageSV. As these both Scrollviews are more similar to each other than they differ, I could steal most of your Code and just define some Differences, for example in your layoutSubviews-Method I dont set the Scrollviews Content Size directly, but call a Method [self giveContentSize], which is defined in MainSV and PageSV differently. Before I did that, I did just define one BOOL “amIMainSV” in MainSV and PageSV differently, but then I had too many checks for this BOOL in layoutSubviews and loadPage and son on and the Code wasnt good readable and not really shorter.
    Right Now the Code works fine in the Simulator but on Device on Rotation some Problems occur (sometimes not updating the frameSize and mostly scrolling to wrong Page)
    Before I write a Book here, I stop and ask 2 Questions:
    1. Would You like to take a Look at my Code and help me a little bit ? Keep in Mind that is more your Code than my Code ;)
    2. Do You have an Idea, why on Rotating the Simulators Page-Calculating does work and the Devices Code scrolls to the wrong Page ? Are there well known Pitfalls ?
    Best Regards, I like your Code cause its short and nicely readable

  6. I load placeholder images into the scrollpages array. I then replace the images in the scrollpages array as they come back from the internet one at a time asyc.

    Is there a way to get the the images in the scrollviewpreview to reload/refresh?

  7. @digitalmasters,
    I think I fail to understand your problem. Are you asking about how to actually set the images that you downloaded and placed in the array to the UIImageView in each placeholder?

  8. Hey, thanks very much for this – really useful.

    I’ve adapted the code to suit my iPad app and it’s working really well.
    Except… the touch events are received on the whole screen, when I only want touches on the BSPreviewScrollView.

    Do you know how I can achieve this? Setting the delegate differently perhaps?

    Thanks again!
    R

  9. No worries – I think I figured it out.
    I just had to comment out :

    if (!CGRectContainsPoint(scrollView.frame, point)) {
    //return self.scrollView; //commenting prevents clicks anywhere on screen from sliding
    }

  10. Do you have your code on GitHub? I made a few modifications to your code like making the pages dequeueable/reusable, implementing adding and deletion of pages on runtime and iOS 4 compatible (blocks).
    If you do I would like to just branch out of your code because yours was the base code

  11. Hi Bjorn,

    In the alert, instead of showing the same message for all images, can we show different message content for each image?

    Like, “Hi I am ‘filename 01′ for 1st image”, and “Hi I am ‘filename 02′ for 2nd image”…etc

  12. Thx a lot for your code.but
    Q:
    do you think app store use the similar way to deal with screenshot? i think it is a lit complex this code.

  13. @Richard
    No worries – I think I figured it out.
    I just had to comment out :
    if (!CGRectContainsPoint(scrollView.frame, point)) {
    //return self.scrollView; //commenting prevents clicks anywhere on screen from sliding
    }

    It works for me also.

  14. Hi Bjorn,
    Your Code was very helpful to learn and also try out different possibilities thanks for sharing, now i thought i ll go ahead and learn how to get different alert messages if different images are pressed. Like the way Alex Lee is asking. Can u please help me out
    Thanks Again

  15. Hi Björn,

    Thanks very much for giving such a helpful example. And special thanks for allowing to use it as we like.. :-) Bravo mate..
    -SPhilip

  16. Hey, awesome example. Thanks!

    I also added one more method to the delegate so an event can be triggered when a scroll occurs.

    -(void)scrollView:(BSPreviewScrollView *)scrollview scrolledToIndex:(int)index;

    Right now this gets called all the time from ‘scrollViewDidScroll:’ but I might modify it further so it only gets called when the currentPage actually is changed…

  17. Hey, This works perfectly in my application, but I want this to be embedded inside a UIScrollView so that I can have content below it. I want it to scroll down so that users can view the content below it but its not working. The content scrolls but the BSPreviewScrollView slide show thing stays over the content in the same place. Any idea how to fix this?

  18. iOS5 exposes the panGestureRecogniser for the scrollview therefore you can target it, and apply the pan gestures out of the scrollview scope. this is particularly important if you are using a uiviewcontroller and therefore hitTest: withEvent: is not accesible.

    To hijack all pan gestures just use

    [self.view addGestureRecognizer:scrollView.panGestureRecognizer];

    with this, you will be able to scroll even if the touch is not on the scrollview

  19. Hello Björn Sållarp,
    Thanks for another great tutorial from you.
    hmm.. can you help me implement a delete method with the this uiscrollview? because you designed it with a exact size with the number of images. Ive done a delete but i guess you can do better :)

    Thanks again

  20. please help me if you know this
    I want to create a xcode tabbar based application.there are 5 tabs.In first page there will be text at the top and at the bottom there is an image with forward and back ward button.And the previews of next and previous is also shown in both sides.when we tap on each buttons it have to scroll to next image or the previous horrizontally.I want to create that in xcode 4.2 storyboard.What I have to do that???please help me its a project for me![it is the screen shot of the another project.i want to do like this][1]

    [1]: http://i.stack.imgur.com/W3XgB.png

  21. First of all, U made it look so easy. Nice integration in my app, looking good. 5 Stars =)
    Just stuck @ I just want to add Left right Arrows and pointed to idea for their Actual Code Implement. THanks in Advance.

  22. Your code is nice, and still works 3 years after writing. I just needed to add some tweaks (as I’m working with ARC), if I’ve got time I’ll make a pull request. Thanks a lot!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>