title: UICollectionView scrollToItem() scrolling to previous cell not working correctly
body: <p>I've been breaking my head over this for the past few weeks and I'm out of ideas..</p>
  <h1>Short background</h1>
  <p>I built my own <code>UICollectionView</code> framework with a custom <code>UICollectionViewFlowLayout</code> for vertical card paging and creating a card stack effect.</p>
  <p><em>Here's a gif of what it looks like:</em></p>
  <p><a href="https://i.stack.imgur.com/DsETw.gif" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/DsETw.gif" alt="framework gif example" /></a></p>
  <h1>Problem</h1>
  <p><strong>I'm trying to implement</strong> a feature that allows the user to <strong>scroll to a specific card at a specific index</strong> (using the  <code>scrollToItem</code> function for example).</p>
  <p>Now <strong>the problem</strong> is that for some reason, <strong>when trying to scroll back to the previous card</strong> it doesn't work correctly.</p>
  <p><strong>Below is a recording of what happens</strong>, I've attached a tapGestureRecognizer to it, when I tap the focussed (currently centered) card, it should scroll to that index - 1 (so the previous card).<br />
  <strong>For some reason it only does this once the <code>frame.origin.y</code> of the bottom card is above the <code>frame.origin.maxY</code> of the focussed (currently centered card)</strong>.</p>
  <p><a href="https://i.stack.imgur.com/CFH8y.gif" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/CFH8y.gif" alt="Problem demo gif" /></a></p>
  <p><em>Notice that I'm tapping twice on the card to make it work, because the <code>frame.origin.y</code> value of the bottom card needs to be lower than the <code>frame.origin.maxY</code> of the top card for some reason for it to work.</em></p>
  <h1>Basic answers I've already tried so far</h1>
  <pre><code>collectionView.scrollToItem(at: convertIndexToIndexPath(for: index), at: .top, animated: animated)
  </code></pre>
  <p>and</p>
  <pre><code>if let frame = collectionView.layoutAttributesForItem(at: convertIndexToIndexPath(for: index))?.frame {
      collectionView.scrollRectToVisible(frame, animated: true)
  }
  </code></pre>
  <h1>Some important stuff to know</h1>
  <p>I've already figured out which line is causing the issue, inside the subclass of <code>UICollectionViewFlowLayout</code> (called <code>VerticalCardSwiperFlowLayout</code>), there is a function called <code>updateCellAttributes</code>, which modifies some properties of the frames to be able to achieve this effect. The issue occurs on the following line:</p>
  <pre><code>let finalY = max(cvMinY, cardMinY)
  </code></pre>
  <p><strong>Here's the function in question including a very extended explanation of how it works at the top in code comments:</strong></p>
  <pre><code>/**
   Updates the attributes.
   Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
   
   Below we'll briefly explain how the effect of scrolling a card to the background instead of the top is achieved.
   Keep in mind that (x,y) coords in views start from the top left (x: 0,y: 0) and increase as you go down/to the right,
   so as you go down, the y-value increases, and as you go right, the x value increases.
   
   The two most important variables we use to achieve this effect are cvMinY and cardMinY.
   * cvMinY (A): The top position of the collectionView + inset. On the drawings below it's marked as &quot;A&quot;.
   This position never changes (the value of the variable does, but the position is always at the top where &quot;A&quot; is marked).
   * cardMinY (B): The top position of each card. On the drawings below it's marked as &quot;B&quot;. As the user scrolls a card,
   this position changes with the card position (as it's the top of the card).
   When the card is moving down, this will go up, when the card is moving up, this will go down.
   
   We then take the max(cvMinY, cardMinY) to get the highest value of those two and set that as the origin.y of the card.
   By doing this, we ensure that the origin.y of a card never goes below cvMinY, thus preventing cards from scrolling upwards.
   
   ```
   +---------+   +---------+
   |         |   |         |
   | +-A=B-+ |   |  +-A-+  | ---&gt; The top line here is the previous card
   | |     | |   | +--B--+ |      that's visible when the user starts scrolling.
   | |     | |   | |     | |
   | |     | |   | |     | |  |  As the card moves down,
   | |     | |   | |     | |  v  cardMinY (&quot;B&quot;) goes up.
   | +-----+ |   | |     | |
   |         |   | +-----+ |
   | +--B--+ |   | +--B--+ |
   | |     | |   | |     | |
   +-+-----+-+   +-+-----+-+
   ```
   
   - parameter attributes: The attributes we're updating.
   */
  fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
      
      guard let collectionView = collectionView else { return }
      
      var cvMinY = collectionView.bounds.minY + collectionView.contentInset.top
      let cardMinY = attributes.frame.minY
      var origin = attributes.frame.origin
      let cardHeight = attributes.frame.height
              
      if cvMinY &gt; cardMinY + cardHeight + minimumLineSpacing + collectionView.contentInset.top {
          cvMinY = 0
      }
      
      let finalY = max(cvMinY, cardMinY)
      
      let deltaY = (finalY - cardMinY) / cardHeight
      transformAttributes(attributes: attributes, deltaY: deltaY)
      
      // Set the attributes frame position to the values we calculated
      origin.x = collectionView.frame.width/2 - attributes.frame.width/2 - collectionView.contentInset.left
      origin.y = finalY
      attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
      attributes.zIndex = attributes.indexPath.row
  }
  
  // Creates and applies a CGAffineTransform to the attributes to recreate the effect of the card going to the background.
  fileprivate func transformAttributes(attributes: UICollectionViewLayoutAttributes, deltaY: CGFloat) {
      
      let translationScale = CGFloat((attributes.zIndex + 1) * 10)
      
      if let itemTransform = firstItemTransform {
          let scale = 1 - deltaY * itemTransform
          
          var t = CGAffineTransform.identity
          
          t = t.scaledBy(x: scale, y: 1)
          if isPreviousCardVisible {
              t = t.translatedBy(x: 0, y: (deltaY * translationScale))
          }
          attributes.transform = t
      }
  }
  </code></pre>
  <p><strong>Here's the function that calls that specific code:</strong></p>
  <pre><code>internal override func layoutAttributesForElements(in rect: CGRect) -&gt; [UICollectionViewLayoutAttributes]? {
      
      let items = NSArray(array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
      for object in items {
          if let attributes = object as? UICollectionViewLayoutAttributes {
              self.updateCellAttributes(attributes)
          }
      }
      return items as? [UICollectionViewLayoutAttributes]
  }
  </code></pre>
  <p><strong>If you want to have a look for yourself, you can download the full project here:</strong>
  <a href="https://github.com/JoniVR/VerticalCardSwiper/archive/master.zip" rel="nofollow noreferrer">https://github.com/JoniVR/VerticalCardSwiper/archive/master.zip</a></p>
  <p><em>(if you do checkout through github, make sure to download the development branch)</em></p>
  <p>The <strong>specific Github issue</strong> with some discussion and possibly extra information can be found <a href="https://github.com/JoniVR/VerticalCardSwiper/issues/23" rel="nofollow noreferrer">here</a>.</p>
  <p><strong>Thank you for your time, any help or information with this problem would be deeply appreciated! Also, please let me know if you have any further question or if any crucial information is missing</strong></p>
  <p><strong>edit 1:</strong>
  Note: The weird effect only happens when scrolling to <code>currentIndex - 1</code>, <code>currentIndex + 1</code> doesn't give any issues, which I think somewhat explains why it's happening on <code>let finalY = max(cvMinY, cardMinY)</code>, but I haven't figured out a proper solution for it yet.</p>