如何将水平分页捕捉到App Store等多行集合视图?

时间:2018-06-24 22:30:18

标签: ios swift uicollectionview uicollectionviewlayout

我想在多行App Store集合视图中复制分页:

enter image description here

到目前为止,我已经将其设计得尽可能接近其外观,包括对上一个和下一个单元格进行窥视,但不知道如何使分页正常工作,因此它会捕捉下一组3 :

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.collectionViewLayout = MultiRowLayout(
        rowsCount: 3,
        inset: 16
    )
}

...

class MultiRowLayout: UICollectionViewFlowLayout {
    private var rowsCount: CGFloat = 0

    convenience init(rowsCount: CGFloat, spacing: CGFloat? = nil, inset: CGFloat? = nil) {
        self.init()

        self.scrollDirection = .horizontal
        self.minimumInteritemSpacing = 0
        self.rowsCount = rowsCount

        if let spacing = spacing {
            self.minimumLineSpacing = spacing
        }

        if let inset = inset {
            self.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
        }
    }

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }
        self.itemSize = calculateItemSize(from: collectionView.bounds.size)
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let collectionView = collectionView,
            !newBounds.size.equalTo(collectionView.bounds.size) else {
                return false
        }

        itemSize = calculateItemSize(from: collectionView.bounds.size)
        return true
    }
}

private extension MultiRowLayout {

    func calculateItemSize(from bounds: CGSize) -> CGSize {
        return CGSize(
            width: bounds.width - minimumLineSpacing * 2 - sectionInset.left,
            height: bounds.height / rowsCount
        )
    }
}

不幸的是,isPagingEnabled上的本机UICollectionView标志仅在单元格为集合视图宽度的100%时才有效,因此用户不会看到,也不会看到上一个和下一个单元格。 / p>

我有一个有效的snap paging functionality,但每页只包含一个项目,而没有这种3行的集合。有人可以帮助使分页的快照分页工作,而不是每页一个项目吗?

3 个答案:

答案 0 :(得分:14)

没有理由仅出于此行为而将UICollectionViewFlowLayout子类化。

UICollectionViewUIScrollView的子类,因此其委托协议UICollectionViewDelegateUIScrollViewDelegate的子类型。这意味着您可以在集合视图的委托中实现UIScrollViewDelegate的任何方法。

在集合视图的委托中,实现scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)将目标内容偏移量舍入到最近的单元格列的左上角。

这是一个示例实现:

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let layout = collectionViewLayout as! UICollectionViewFlowLayout
    let bounds = scrollView.bounds
    let xTarget = targetContentOffset.pointee.x

    // This is the max contentOffset.x to allow. With this as contentOffset.x, the right edge of the last column of cells is at the right edge of the collection view's frame.
    let xMax = scrollView.contentSize.width - scrollView.bounds.width

    if abs(velocity.x) <= snapToMostVisibleColumnVelocityThreshold {
        let xCenter = scrollView.bounds.midX
        let poses = layout.layoutAttributesForElements(in: bounds) ?? []
        // Find the column whose center is closest to the collection view's visible rect's center.
        let x = poses.min(by: { abs($0.center.x - xCenter) < abs($1.center.x - xCenter) })?.frame.origin.x ?? 0
        targetContentOffset.pointee.x = x
    } else if velocity.x > 0 {
        let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
        // Find the leftmost column beyond the current position.
        let xCurrent = scrollView.contentOffset.x
        let x = poses.filter({ $0.frame.origin.x > xCurrent}).min(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? xMax
        targetContentOffset.pointee.x = min(x, xMax)
    } else {
        let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget - bounds.size.width, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
        // Find the rightmost column.
        let x = poses.max(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? 0
        targetContentOffset.pointee.x = max(x, 0)
    }
}

// Velocity is measured in points per millisecond.
private var snapToMostVisibleColumnVelocityThreshold: CGFloat { return 0.3 }

结果:

demo

您可以在这里找到我的测试项目的完整源代码:https://github.com/mayoff/multiRowSnapper

答案 1 :(得分:3)

有了iOS 13,这变得容易多了!

在iOS 13中,您可以使用CREATE TEMP FUNCTION test(num FLOAT64, decimalPlaces INT64) RETURNS FLOAT64 LANGUAGE js AS """ var d = decimalPlaces || 0; var m = Math.pow(10, d); var n = +(d ? num * m : num).toFixed(8); // Avoid rounding errors var i = Math.floor(n), f = n - i; var e = 1e-8; // Allow for rounding errors in f var r = (f > 0.5 - e && f < 0.5 + e) ? ((i % 2 == 0) ? i : i + 1) : Math.round(n); return d ? r / m : r; """; SELECT ROUND(1.525,2)

这引入了两个概念,在这里我将尝试给出要点,但是我认为这很有价值!

概念

在CompositionalLayout中,您可以使用3个实体来指定尺寸。您可以使用绝对值,分数值(例如一半)或估计值来指定尺寸。这三个实体是:

  • 项目(UICollectionViewCompositionalLayout

您的单元格大小。分数大小是相对于项目所属的组而言的,它们可以考虑父项的宽度或高度。

  • 组(NSCollectionLayoutItem

组允许您创建一组项目。在该应用商店的示例中,一个组是一列,包含3个项目,因此您的项目应占该组的0.33高度。然后,您可以说该小组的身高为300。

  • 部分(NSCollectionLayoutGroup

部分声明该组将如何重复自身。在这种情况下,您可以说该部分是水平的。

创建布局

您使用带有一个接受节索引和NSCollectionLayoutSection的闭包来创建布局。这很有用,因为您可以针对每个特征(例如,在iPad上可以有不同的东西)和针对每个部分的索引(例如,您可以有一个水平滚动的部分,而另一个垂直放置的东西)具有不同的布局。

NSCollectionLayoutEnvironment

应用商店示例

在应用商店中,您有really good video的Paul Hudson的Hacking with Swift对此进行了解释。他也有一个repo

但是,我将代码放在此处,以免丢失:

func createCollectionViewLayout() {
   let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
      return self.createAppsColumnsLayout()
   }

   let config = UICollectionViewCompositionalLayoutConfiguration()
   config.scrollDirection = .vertical
   config.interSectionSpacing = 31

   layout.configuration = config
   return layout
}

最后,您只需要设置布局即可

func createAppsColumnsLayout(using section: Section) -> NSCollectionLayoutSection {
   let itemSize = NSCollectionLayoutSize(
      widthDimension: .fractionalWidth(1), 
      heightDimension: .fractionalHeight(0.33)
   )

   let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
   layoutItem.contentInsets = NSDirectionalEdgeInsets(
      top: 0, 
      leading: 5, 
      bottom: 0, 
      trailing: 5
   )

   let layoutGroupSize = NSCollectionLayoutSize(
       widthDimension: .fractionalWidth(0.93),
       heightDimension: .fractionalWidth(0.55)
   )
   let layoutGroup = NSCollectionLayoutGroup.vertical(
      layoutSize: layoutGroupSize, 
      subitems: [layoutItem]
   )

   let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
   layoutSection.orthogonalScrollingBehavior = .groupPagingCentered

   return layoutSection
}

collectionView.collectionViewLayout = createCompositionalLayout() 附带的一个很酷的东西是不同的页面机制,例如UICollectionViewCompositionalLayout,但是我认为这个答案已经足够长,可以解释它们之间的区别了。

答案 2 :(得分:1)

UICollectionView( .scrollDirection = .horizo​​ntal )可以用作外部容器,用于将每个列表包含在其单独的UICollectionViewCell中。

依次使用单独的UICollectionView( .scrollDirection = .vertical )构建每个列表。

使用 collectionView.isPagingEnabled = true

在外部UICollectionView上启用分页

它是一个布尔值,该值确定是否为滚动视图启用了分页。 如果此属性的值为true,则当用户滚动时,滚动视图会在滚动视图边界的倍数处停止。默认值为false。

注意:重置左右内容插图以消除每页两侧的多余间距。 例如collectionView?.contentInset = UIEdgeInsetsMake(0,0,0,0)