I'm going to describe the effect I want to achieve, and then I'll give detail on how I'm currently trying to implement this and what's wrong with its behaviour as it stands. I'll also mention another approach I've looked at but couldn't make work at all.
The most relevant code is inline at the bottom of the question for quick access. You can Download a zip of the source or get the project as a Mercurial Repository at BitBucket. The project now incorporates the fixes from the answer below. If you want the broken version initially provided, it is tagged with "initial-buggy-version"
The project is a minimal-ish proof of concept / spike to evaluate whether the effect is viable, so it's fairly light and simple!
The App will display a large number of discrete rows of information that form a vertical table. The table will be vertically scrollable by the user. This is standard behaviour with a UITableView
, and you can use a UICollectionView
too. However, the App must also support pinch scaling. When you pinch zoom on the table, all of the lines should squash together. As you stretch, all of the lines should pull apart.
In my proof of concept the individual cells are not resized, they are just repositioned closer together or further apart. This is intentional: I don't believe it's critical to validating the idea's feasibility.
Here're screen grabs showing how the current App looks zoomed out and zoomed in:
I'm using a UICollectionView
with a custom UICollectionViewLayout
subclass. The layout positions the UICollectionViewCells
in a nice wobbly sine wave down the middle of the screen. Each UICollectionViewCell
is just a container for a UILabel
holding the indexPath
row.
The UICollectionViewLayout
subclass has a parameter to set the vertical spacing between each cell it describes to the UICollectionView
and adjusting this allows the table to be squashed or stretched vertically as desired.
My UICollectionViewController
subclass has a UIPinchGestureRecognizer
. When the recogniser detects scale changes, the vertical cell spacing in the UICollectionView
's layout is changed accordingly.
Without further consideration, scaling would occur from the top of the content, rather than about the centre of the touch gesture. The UICollectionView
's contentOffset
property is adjusted during pinching to provide this feature.
The gesture recogniser also needs to accommodate drags that occur while pinching. This is also handled by changing the UICollectionView
's contentOffset
. Some additional code allows for the centre point of the touch gesture changing about as fingers are added to / removed from the gesture.
Note that UICollectionView
, being a UIScrollView
subclass, has its own UIPanGestureRecognizer
which interacts with the UIPinchGestureRecogniser
added by me. I am not sure if this is causing a problem or not.
I have added code to disable the UICollectionView
's built in scrolling during my pinch gesture, but this doesn't seem to make much difference. I tried to use gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
to make my UIPinchGestureRecognizer
fail the built in UIPanGestureRecognizer
, but this instead seemed to stop my pinch recognizer working at all. I don't know if this is me being stupid, or a bug in iOS.
As mentioned before, current the UICollectionViewCell
s are not resized. They are just repositioned. This is intentional. I don't think it's important for validating this concept.
The working bits work quite well. You can drag the table up and down. During a drag you can add a finger and start a pinch, then release a finger and continue the drag, then add and pinch, etc. It's all pretty smooth. On an original iPhone 5 it smoothly supports pinch and pan with > 200 views on screen.
If you try to pinch in and out when the top or the bottom of the view is on screen, it all goes a bit mad.
These two fight with each other during the pinch gesture, which makes the content violently flicker up and down (which I definitely don't want!).
The UICollectionView
's default scrolling has deceleration if you let go while scrolling, and also smoothly bounces the content back when you scroll outside of it. These aren't handled at all currently.
UICollectionView
, being a UIScrollView
should have a built in UIPinchGestureRecogniser
if it's set up correctly to support zooming. I wondered if I could harness this instead of having my own UIPinchGestureRecogniser
. I tried to set this up by setting min and max scales, and adding my controller's pinch handler. However, I don't really understand what I should be returning from my implementation of viewForZoomingInScrollView:
, so I'm just creating a dummy view with [[UIView alloc] initWithFrame: [[self collectionView] bounds]]
. It makes the scroll view "collapse" to a single line, which isn't what I'm after!
This is a long question, so thank you for reading it. Thank you even more if you can help with an answer. I'm sorry if a lot of what I've said or added is irrelevant!
// STViewController.m
#import "STViewController.h"
#import "STDataColumnsCollectionViewLayout.h"
#import "STCollectionViewLabelCell.h"
@interface STViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, assign) CGFloat pinchStartVerticalPeriod;
@property (nonatomic, assign) CGFloat pinchNormalisedVerticalPosition;
@property (nonatomic, assign) NSInteger pinchTouchCount;
-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser;
@end
@implementation STViewController
-(void) viewDidLoad
{
[[self collectionView] registerClass: [STCollectionViewLabelCell class] forCellWithReuseIdentifier: [STCollectionViewLabelCell className]];
UICollectionView *const collectionView = [self collectionView];
[collectionView setAllowsSelection: NO];
[_pinchRecogniser addTarget: self action: @selector(handlePinch:)];
[_pinchRecogniser setDelegate: self];
[_pinchRecogniser setCancelsTouchesInView:YES];
[[self view] addGestureRecognizer: _pinchRecogniser];
}
#pragma mark -
-(NSInteger) collectionView: (UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
return 800;
}
-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
STCollectionViewLabelCell *const cell = [[self collectionView] dequeueReusableCellWithReuseIdentifier: [STCollectionViewLabelCell className] forIndexPath: indexPath];
[[cell label] setText: [NSString stringWithFormat: @"%d", [indexPath row]]];
return cell;
}
#pragma mark -
-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
#pragma mark -
-(void) handlePinch: (UIPinchGestureRecognizer *) pinchRecogniser
{
UICollectionView *const collectionView = [self collectionView];
STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];
if(([pinchRecogniser state] == UIGestureRecognizerStateBegan) || ([pinchRecogniser numberOfTouches] != _pinchTouchCount))
{
const CGFloat normalisedY = [pinchRecogniser locationInView: collectionView].y / [layout collectionViewContentSize].height;
_pinchNormalisedVerticalPosition = normalisedY;
_pinchTouchCount = [pinchRecogniser numberOfTouches];
}
switch ([pinchRecogniser state])
{
case UIGestureRecognizerStateBegan:
{
NSLog(@"Began");
_pinchStartVerticalPeriod = [layout verticalPeriod];
[collectionView setScrollEnabled: NO];
break;
}
case UIGestureRecognizerStateChanged:
{
NSLog(@"Changed");
STDataColumnsCollectionViewLayout *const layout = (STDataColumnsCollectionViewLayout *)[self collectionViewLayout];
const CGFloat newVerticalPeriod = _pinchStartVerticalPeriod * [pinchRecogniser scale];
[layout setVerticalPeriod: newVerticalPeriod];
[[self collectionViewLayout] invalidateLayout];
const CGPoint dragCenter = [pinchRecogniser locationInView: [collectionView superview]];
const CGFloat currentY = _pinchNormalisedVerticalPosition * [layout collectionViewContentSize].height;
[collectionView setContentOffset: CGPointMake(0, currentY - dragCenter.y) animated: NO];
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
{
[collectionView setScrollEnabled: YES];
}
default:
break;
}
}
@end
Some very minor tweaks to the above code have resolved What Doesn't Work 1 & What Doesn't Work 2 in the question.
I have added the following lines in to the viewDidLoad
method of my UICollectionViewController
:
[collectionView setMinimumZoomScale: 0.25];
[collectionView setMaximumZoomScale: 4];
I've also updated the example project so that instead of text labels, the view is made of little circles. As you zoom in and out, these are resized. Here's what it looks like now (zoomed out and zoomed in):
During a zoom the views for the circles are not redrawn, but just interpolated from their pre-zoom size. The redraw is postponed until the zoom finishes. Here's a capture of how that looks after a zoom in of several times:
It would be great to have the redrawing during zoom happen in a background thread so that the artefacts are less noticeable, but that's well out of the scope of this question and I've not worked on it yet either.
You can find the entire project, with fixes, on Bit Bucket so you can grab the files there.
I was hoping that with this question answered, I'd have lots of new certainty about UIScrollView
zooming. I don't.
From what I've read about UIScrollView, this "fix" should not have made any difference and it should have already worked in the first place anyway.
A UIScrollView
isn't supposed to enable scrolling until you give it a delegate that implements viewForZoomingInScrollView:
, which I've not done.