MaherKSantina / MSPeekCollectionViewDelegateImplementation

A custom paging behavior that peeks the previous and next items in a collection view
https://medium.com/@maher.santina90/how-to-create-cells-that-peek-on-the-sides-like-ios-11-app-store-ef4bb54c0c7d
MIT License
358 stars 32 forks source link

Support for more than one item in the cross-axis #38

Open gcox opened 5 years ago

gcox commented 5 years ago

Currently, this lib forces the cross axis size of a cell to match that of the collection view. The App Store has another type of horizontally peeking collection view where multiple cells are displayed vertically in each page. The lib could allow specifying the number of cross-axis items to show, then use that value to calculate the appropriate item size in the collectionView:layout:sizeForItemAt: delegate func.

Screenshot of the UI I'm referring to img_0906

MaherKSantina commented 5 years ago

Thanks @gcox for raising this issue! That's a good idea to implement. However, it's a bit challenging implement as we need to take into account inter-item/cell spacing and size for items. Also, calculation of current index and scrolling to items at specific indices will change. I'd consider this as nice to have since you can do a similar functionality by implementing a collection view cell that has a UITableView/UIStackView which shows x items at a time. If you have any ideas of how to make it simpler to implement this functionality it'd be of great help. Please let me know what you think

gcox commented 5 years ago

@MaherKSantina Here's the basic implementation details. It is not thoroughly tested and I'm sure I'm missing some edge cases, just want to see if you had any other thoughts on this. I can submit a PR later this week if you don't see any glaring issues here.

fileprivate lazy var itemCrossLength: (UIView) -> CGFloat = {
  var length: CGFloat = self.scrollDirection.crossLength(for: $0)
  let allItemsLength = (length
    - (CGFloat(self.numberOfItemsToShowInCrossAxis + 1) * (self.cellSpacing))
    - 2)
  let finalLength = allItemsLength / CGFloat(self.numberOfItemsToShowInCrossAxis)
  return max(0, finalLength)
}

open func scrollView(_ scrollView: UIScrollView, indexForItemAtContentOffset contentOffset: CGPoint) -> Int {
  let mainAxisItemLength = itemLength(scrollView) + cellSpacing
  guard mainAxisItemLength > 0 else {
    return 0
  }
  let mainAxisOffset = self.scrollDirection.value(for: contentOffset)
  guard numberOfItemsToShowInCrossAxis > 1 else {
    return Int(round(mainAxisOffset / mainAxisItemLength))
  }
  let crossAxisItemLength = itemCrossLength(scrollView) + cellSpacing
  let crossAxisOffset = self.scrollDirection.crossValue(for: contentOffset)
  let minimumIndex = Int(round(mainAxisOffset / mainAxisItemLength)) * numberOfItemsToShowInCrossAxis
  let index = minimumIndex + Int(round(crossAxisOffset/crossAxisItemLength))
  return index
}

open func scrollView(_ scrollView: UIScrollView, contentOffsetForItemAtIndex index: Int) -> CGFloat {
  return CGFloat(index / numberOfItemsToShowInCrossAxis) * (itemLength(scrollView) + cellSpacing)
}

// Assumes we want the full height taken up by equally sized items
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  var defaultSize = collectionView.frame.size
  if numberOfItemsToShowInCrossAxis > 1 {
    let crossAxisItems = CGFloat(numberOfItemsToShowInCrossAxis)
    let crossAxisGapTotal = (crossAxisItems - 1) * cellSpacing

    switch scrollDirection {
    case .horizontal:
      defaultSize.height = (defaultSize.height - crossAxisGapTotal) / crossAxisItems
    case .vertical:
      defaultSize.width = (defaultSize.width - crossAxisGapTotal) / crossAxisItems
    }
  }
  return scrollDirection.size(for: itemLength(collectionView), defaultSize: defaultSize)
}

public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  ...

  // Only line that needs to change in this function
  let numberOfItemsToScroll = getNumberOfItemsToScroll(scrollDistance: currentScrollDistance, scrollWidth: itemLength(scrollView)) *
    numberOfItemsToShowInCrossAxis

  ...
}

That code depends on a couple of new extension methods on UICollectionViewScrollDirection just to return the 'crossLength', 'crossValue' numbers. Left them out since they're so simple.

MaherKSantina commented 5 years ago

Ohh @gcox thank you for the code snippet! I'll definitely take a look at it in the weekend

Sk8er22 commented 4 years ago

this is actually supported?

MaherKSantina commented 4 years ago

@Sk8er22 sadly it's not implemented yet. I was focusing on other features that I thought were more important. I deprioritized this one because you can achieve the same behavior by creating a collection view cell that has 3 items. I'm thinking about the complexity this feature adds to the library and it feels like there are a lot of things to take into consideration so I'm still hesitant about it.

Do you think you can achieve the same behavior by creating a collection view cell with a vertical stack view and add the views in it?

Sk8er22 commented 4 years ago

I did it doing a custom big cell that contains all the possible stacks. Thanks anyway!

MaherKSantina commented 4 years ago

@Sk8er22 sounds great! I'll add a help-wanted tag in the meantime

Have a great day!