滚动到顶部时,UIScrollView 出现故障和摇晃

UIScrollView is glitchy and shaky when scrolls to the top

我有一个 UIViewController,其中包含一个 UICollectionView 和一个 UIView 作为视图 header。

我想在视图滚动时折叠 header。我目前正在通过在变量中捕获我的 header 的顶部锚点并使用 scrollViewDidScroll 设置此值的 constant 来做到这一点。

  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    headerTopAnchor.constant = max(-headerView.frame.height, -scrollView.contentOffset.y)
  }

这有效,因为 collection 视图向上滚动,header 滚动到屏幕外,反之亦然。

但是,如果 collection 视图中的内容完全 不适合 - 如果只有半个左右的单元格滚出屏幕,有一种奇怪的颤抖行为。

如果我向 scrollViewDidScroll 添加打印语句,我可以看到 collection 视图有少量过度滚动,这导致顶部锚点多次发生少量变化

  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    print(max(-headerView.frame.height, -scrollView.contentOffset.y))
    headerTopAnchor.constant = max(-headerView.frame.height, -scrollView.contentOffset.y)
  }

如何防止这种行为?

我已经包含了一个可以演示问题的视图控制器 -

final class TestViewController: UIViewController {

  private let headerView: UIView = {
    let view = UIView(frame: .zero)
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = .systemTeal
    return view
  }()

  private(set) lazy var collectionView: UICollectionView = {
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    collectionView.backgroundColor = .clear
    collectionView.dataSource = self
    collectionView.delegate = self
    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
    return collectionView
  }()

  private lazy var headerTopAnchor = NSLayoutConstraint()

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemGray
    [headerView, collectionView].forEach(view.addSubview(_:))
    NSLayoutConstraint.activate([
      headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      headerView.heightAnchor.constraint(equalToConstant: 180),

      collectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
      collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
      collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    ])

    headerTopAnchor = headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
    headerTopAnchor.isActive = true
  }
}

extension TestViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    // return 300 // no problem
    return 10 // problem :(
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath)
    cell.backgroundColor = indexPath.item % 2 == 0 ? .darkGray : .lightGray
    return cell
  }
}

extension TestViewController: UICollectionViewDelegateFlowLayout {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    print(max(-headerView.frame.height, -scrollView.contentOffset.y))
    headerTopAnchor.constant = max(-headerView.frame.height, -scrollView.contentOffset.y)
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return .init(width: collectionView.frame.width, height: 60)
  }
}

似乎 UIScrollView 有一个 bounces 属性 默认设置为 true

来自文档:

If the value of this property is true, the scroll view bounces when it encounters a boundary of the content. Bouncing visually indicates that scrolling has reached an edge of the content. If the value is false, scrolling stops immediately at the content boundary without bouncing. The default value is true.

这解释了在应用动画的同时在触及边界时被顶部约束纠正的抖动行为。

我在 viewDidLoad 中设置了 collectionView.bounces = false,它现在工作正常。