Code, Code, Revolution!
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:
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.
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;
@endBSPreviewScrollView.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];
}
@endDownload the sample application utilizing BSPreviewScrollView
With this blog I try to provide useful tips and solutions for programming .NET, Objective-C and more. My name is Björn Sållarp, and I love writing code.
It's now available on AppStore. It's free and open source. Read more about the app here: Swedish / English
Stian
August 13th, 2010 at 5:47 pm
Thanks,
I just added the hitTest method to my project and everything works as I planned
Ankit
August 25th, 2010 at 3:15 pm
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.
asdf
September 3rd, 2010 at 12:53 pm
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!
Björn Sållarp
September 18th, 2010 at 3:22 pm
@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.
Fatih YASAR
September 23rd, 2010 at 7:45 am
Björn,
Your code helped me a lot, It saved my times.
Thank you for this example.
Kevin Quinn
September 28th, 2010 at 1:08 pm
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.
lenni
October 11th, 2010 at 7:25 pm
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
DigitalMasters
October 20th, 2010 at 4:58 am
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?
Björn Sållarp
October 20th, 2010 at 9:00 am
@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?
Richard
October 22nd, 2010 at 12:53 pm
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
Richard
October 22nd, 2010 at 4:34 pm
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
}
Emerson Malca
October 27th, 2010 at 6:16 pm
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
Alex Lee
October 31st, 2010 at 11:00 am
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
Dittimon
November 10th, 2010 at 8:12 am
Legend! Thanks mate!
bicbac
December 10th, 2010 at 4:25 am
@ Emerson Malca, can you post your version of code somewhere so that we can look?
pink
December 30th, 2010 at 2:40 pm
Thank you! your code helped me a lot
willbin
February 10th, 2011 at 7:09 am
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.
Umaid Saleem
February 17th, 2011 at 3:03 pm
How to add 2 scrollView in same view, I added but only one is working and when I integrate in my own application my view hangs up and only one scrollview is working. Please find my source code here.
http://rapidshare.com/files/448421646/ScrollViewPreview-1.zip
Umaid Saleem
February 18th, 2011 at 8:35 am
@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.
stone
February 27th, 2011 at 3:51 am
thanks a lot! learing
Pramod Chinnapla
April 7th, 2011 at 1:08 pm
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
Maxs
April 22nd, 2011 at 8:38 pm
Thanks! It is great
S.Philip
May 25th, 2011 at 6:33 am
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
Paul Mans
August 6th, 2011 at 6:58 pm
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…
Ian
September 15th, 2011 at 9:52 am
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?
Gianni
September 27th, 2011 at 12:58 pm
Great job!
I’m using it in my app!
Ali
October 10th, 2011 at 11:23 am
Thanks .. !!!
Spidey2010
November 21st, 2011 at 2:49 pm
Hi, great tutorial. How would one go about adding a PageControl to the bottom of the page?
Thanks
edwin b
December 13th, 2011 at 4:06 pm
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
Nico
February 21st, 2012 at 5:25 pm
thank you! thank you! thank you!
aL
March 7th, 2012 at 5:13 am
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