title: Adding pinch zoom to a UICollectionView
body: <h1>Intro</h1>
  
  <p>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.</p>
  
  <p>The most relevant code is inline at the bottom of the question for quick access. You can <strong><a href="https://bitbucket.org/benjohn/uicollectionviewspike/get/default.zip">Download a zip of the source</a></strong> or get the project as a <strong><a href="https://bitbucket.org/benjohn/uicollectionviewspike">Mercurial Repository</a></strong> at BitBucket. The project now incorporates the fixes from the answer below. If you want the broken version initially provided, it is tagged with <em>"initial-buggy-version"</em></p>
  
  <p>The project is a minimal-ish proof of concept / spike to evaluate whether the effect is viable, so it's fairly light and simple!</p>
  
  <h1>Desired Effect</h1>
  
  <p>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 <code>UITableView</code>, and you can use a <code>UICollectionView</code> too. However, the App must also support pinch scaling. When you pinch zoom on the table, <strong>all</strong> of the lines should squash together. As you stretch, <strong>all</strong> of the lines should pull apart.</p>
  
  <p>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.</p>
  
  <p>Here're screen grabs showing how the current App looks zoomed out and zoomed in:</p>
  
  <p><img src="https://i.stack.imgur.com/IsPuq.png" alt="Zoomed in image" title="Zoomed in view">
  <img src="https://i.stack.imgur.com/tpTQh.png" alt="Zoomed out image" title="Zoomed out view"></p>
  
  <h1>Current Implementation</h1>
  
  <p>I'm using a <code>UICollectionView</code> with a custom <code>UICollectionViewLayout</code> subclass. The layout positions the <code>UICollectionViewCells</code> in a nice wobbly sine wave down the middle of the screen. Each <code>UICollectionViewCell</code> is just a container for a <code>UILabel</code> holding the <code>indexPath</code> row.</p>
  
  <p>The <code>UICollectionViewLayout</code> subclass has a parameter to set the vertical spacing between each cell it describes to the <code>UICollectionView</code> and adjusting this allows the table to be squashed or stretched vertically as desired.</p>
  
  <p>My <code>UICollectionViewController</code> subclass has a <code>UIPinchGestureRecognizer</code>. When the recogniser detects scale changes, the vertical cell spacing in the <code>UICollectionView</code>'s layout is changed accordingly. </p>
  
  <p>Without further consideration, scaling would occur from the top of the content, rather than about the centre of the touch gesture. The <code>UICollectionView</code>'s <code>contentOffset</code> property is adjusted during pinching to provide this feature.</p>
  
  <p>The gesture recogniser also needs to accommodate drags that occur while pinching. This is also handled by changing the <code>UICollectionView</code>'s <code>contentOffset</code>. Some additional code allows for the centre point of the touch gesture changing about as fingers are added to / removed from the gesture.</p>
  
  <p>Note that <code>UICollectionView</code>, being a <code>UIScrollView</code> subclass, has its own <code>UIPanGestureRecognizer</code> which interacts with the <code>UIPinchGestureRecogniser</code> added by me. I am not sure if this is causing a problem or not.</p>
  
  <p>I have added code to disable the <code>UICollectionView</code>'s built in scrolling during my pinch gesture, but this doesn't seem to make much difference. I tried to use <code>gestureRecognizer:shouldRequireFailureOfGestureRecognizer:</code> to make my <code>UIPinchGestureRecognizer</code> fail the built in <code>UIPanGestureRecognizer</code>, 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.</p>
  
  <p>As mentioned before, current the <code>UICollectionViewCell</code>s are not resized. They are just repositioned. This is intentional. I don't think it's important for validating this concept.</p>
  
  <h1>What Works</h1>
  
  <p>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.</p>
  
  <h1>What doesn't work 1</h1>
  
  <p>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.</p>
  
  <ul>
  <li>On scrolls, the view is allowed to drag so that it is pulled beyond the visible content (which I want, as it's standard behaviour for a list of data on iOS).</li>
  <li>On scale changes, however, the view is snapped back so that the content is clamped in to the screen (I don't want this to happen).</li>
  </ul>
  
  <p>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!).</p>
  
  <h1>What doesn't work 2</h1>
  
  <p>The <code>UICollectionView</code>'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.</p>
  
  <ul>
  <li>If you release the pinch gesture while scrolling, it just stops.</li>
  <li>If you scroll beyond the content with the pinch gesture and then release, it stays where it is and doesn't bounce back. When you then begin a scroll again it jumps the content back.</li>
  </ul>
  
  <h1>Things I've tried but couldn't get to work</h1>
  
  <p><code>UICollectionView</code>, being a <code>UIScrollView</code> should have a built in <code>UIPinchGestureRecogniser</code> if it's set up correctly to support zooming. I wondered if I could harness this instead of having my own <code>UIPinchGestureRecogniser</code>. 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 <code>viewForZoomingInScrollView:</code>, so I'm just creating a dummy view with <code>[[UIView alloc] initWithFrame: [[self collectionView] bounds]]</code>.  It makes the scroll view "collapse" to a single line, which isn't what I'm after!</p>
  
  <h1>Finally (Before the code)</h1>
  
  <p>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!</p>
  
  <h1>Code for the view controller</h1>
  
  <pre><code>//  STViewController.m
  #import "STViewController.h"
  #import "STDataColumnsCollectionViewLayout.h"
  #import "STCollectionViewLabelCell.h"
  
  @interface STViewController () &lt;UIGestureRecognizerDelegate&gt;
  @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
  </code></pre>