本篇文章记录使用 NSCollectionLayoutSection 类的 orthogonalScrollingBehavior 属性的过程中遇到的问题。

通过设置该属性可以控制对应 Section 的滑动效果。

在阅读以下内容时,我将默认您已经掌握了 UICollectionViewCompositionalLayout 的基础用法,不再对一些细节进行补充说明。

Orthogonal Scroll View

这一节我们会涉及到两个系统的私有类型:_UICollectionViewOrthogonalScrollView_UICollectionViewOrthogonalScrollerEmbeddedScrollView

因为这两个类型的名称太长,同时会多次重复提及,故下文使用 _UIOrthogonalScrollView 代替。

这两个类型可以看作是一个,早期 Apple 使用的是 _UICollectionViewOrthogonalScrollerEmbeddedScrollView 这个名称,后期在某个版本中改为 _UICollectionViewOrthogonalScrollView

orthogonalScrollingBehavior 的默认值是 .none,当我们将其设置为其他值后,系统就会在 UICollectionView 上添加一层类型为 _UIOrthogonalScrollViewUIScrollView 子类作为 Cell 的父视图,如下图所示:

此时 Cell 的父视图不再是 UICollectionVIew

我们测试过以下情况:

  • UICollectionViewCompositionalLayout 只有一种布局。
  • NSCollectionLayoutGroup 的滑动方向和 UICollectionView 的滑动方向一致或不一致。
  • NSCollectionLayoutSection 的尺寸和 UICollectionView 相等或不相等。

在这三种情况下,只要修改了 orthogonalScrollingBehavior 属性,就会添加 _UIOrthogonalScrollView 视图。

在正式展开说明之前插一句:本文后面所提及的内容,都是在下面这个布局的基础上进行修改的:

let itemSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(1/3),
    heightDimension: .absolute(50))

let item = NSCollectionLayoutItem(layoutSize: itemSize)

let group = NSCollectionLayoutGroup.horizontal(
    layoutSize: .init(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(0.5)),
    subitems: [item])

let section = NSCollectionLayoutSection(group: group)

return section

获取时机

那么该如何获取该视图呢,经过不完全测试,列表不包含任何 Cell 时,不会添加该视图。
该视图可在 viewDidLayoutSubviews 方法中通过遍历子视图获得。

/// `orthogonalScrollView` 可能的类型
///
/// 目前尚无法确定哪个版本的系统使用了哪个类型,所以为了稳妥起见,使用数组进行判断
private var orthogonalScrollViewTypes: [String] {
    [
        "_UICollectionViewOrthogonalScrollView",
        "_UICollectionViewOrthogonalScrollerEmbeddedScrollView"
    ]
}

var orthogonalScrollView: UIScrollView? {
    subviews.first { orthogonalScrollViewTypes.contains("\(type(of: $0))") } as? UIScrollView
}

clipsToBounds

_UIOrthogonalScrollViewclipsToBounds 属性默认是 false,这会导致一个问题:

如果您的 Section 设置了 contentInsets 之类的属性,导致比 UICollectionView 小,那么在滑动 _UIOrthogonalScrollView 的时候,其内容会超出 Section 的范围。

iOS 15 及以上

_UIOrthogonalScrollView 是严格跟着 Section 范围走的,例如下图:

蓝色选中的范围是 _UIOrthogonalScrollView,可以看到滑动的时候 Cell 明显超出了它的范围。

上文有提到获取 _UIOrthogonalScrollView 的时机,您可以在获取后通过手动将 clipsToBounds 设置为 ture 来解决该问题。

iOS 15 以下

经不完全测试,iOS 14.7.1 及以下版本符合该小节内容。iOS 14.7.1 - iOS 15 之间的版本没有经过测试,不确定符合上一小节还是该小节。

_UIOrthogonalScrollView 的范围是跟着 UICollectionView 走的,由下图所示:

和上一张图对比之后,可以很明显的发现区别(代码相同)。值得一提的是此时 _UIOrthogonalScrollViewcontentInset.zero

此时设置 clipsToBounds 就没有用了。

跟随滑动

如果您准备用 UICollectionViewCompositionalLayout 实现类似如上图的效果,那么您需要确保滑块是添加到 _UIOrthogonalScrollView 上。

考虑到该视图的获取时机,我建议您还是改用 UICollectionViewFlowLayout 实现。

.continuous

原本以为配合 UICollectionLayoutSectionOrthogonalScrollingBehavior.continuous,最终可以达到和 UICollectionViewCompositionalLayout 一样的效果,但是事实是并不会。

本文使用的例子里包含一个横滑的 group,item 的宽度是 group 的 1/3,group 的宽度和 UICollectionView 宽度一致。但是如果把 item 的宽度改为某个固定的值,例如 50,那么就会发现:屏幕上只会显示完整的 Cell。如下图所示:

右侧剩余的宽度不够第四个 Cell 显示出来,那么它就会隐藏掉,在后面再显示:

如果不修改 orthogonalScrollingBehavior(即值为 .none),则不会有该问题,但是 Cell 在超出屏幕后会换行展示,因为没有 _UIOrthogonalScrollView ...

所以建议这种需求还是使用 UICollectionViewFlowLayout 组合实现。
或者您知道什么更好的方式,请在评论区留言告诉我,谢谢。