kye
kye

Reputation: 2246

UIScrollView delegate methods not called when UICollectionViewCompositionalLayout is set

I currently have a UICollectionView using UICollectionViewCompositionalLayout. I would like to animate some views within the current visible cells while scrolling / scrolling stops.

Unfortunately it seems setting orthogonalScrollingBehavior on a section to anything but .none hijacks the UICollectionView accompanying UIScrollView delegate methods.

Was wondering if there're any current workaround for this? To get the paging behaviour and UIScrollView delegate?

Setup layout

  enum Section {
    case main
  }

  override func awakeFromNib() {
    super.awakeFromNib()
    collectionView.collectionViewLayout = createLayout()
    collectionView.delegate = self
  }

  func configure() {
    snapshot.appendSections([.main])
    snapshot.appendItems(Array(0..<10))
    dataSource.apply(snapshot, animatingDifferences: false)
  }

 private func createLayout() -> UICollectionViewLayout {
    let leadingItem = NSCollectionLayoutItem(
      layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0))
    )

    leadingItem.contentInsets = .zero

    let containerGroup = NSCollectionLayoutGroup.horizontal(
      layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0)
      ),
      subitems: [leadingItem])

    let section = NSCollectionLayoutSection(group: containerGroup)
    section.orthogonalScrollingBehavior = .groupPaging // WOULD LIKE PAGING & UISCROLLVIEW TO ALSO BE FIRED

    let config = UICollectionViewCompositionalLayoutConfiguration()
    config.scrollDirection = .horizontal

    let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)
    return layout
  }

UICollectionViewDelegate

extension SlidingCardView: UICollectionViewDelegate {

  func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // THIS IS FIRED BUT UISCROLLVIEW METHODS NOT
  }

  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    print(111)
  }


  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    print("1111111")
  }
}

Upvotes: 14

Views: 5104

Answers (4)

Kwalker108
Kwalker108

Reputation: 492

Here is a solution for determining which cell is in the center of the screen:

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
    guard let self = self else { return }

    for visibleCell in self.collectionView.visibleCells {
      let collectionViewCenterPoint = self.collectionView.center
      
      if let relativePoint = visibleCell.superview?.convert(collectionViewCenterPoint, from: nil),
          visibleCell.frame.contains(relativePoint)
      {
        // visibleCell is in the center of the view.
      } else {
        // visibleCell is outside the center of the view.
      }
    }
  }

Upvotes: 3

apouche
apouche

Reputation: 9973

Following @Stoyan answer, I fine tuned the class to be compatible with producition code by not looking for private APIs. Simply looking at all UIScrollView subclasses.

Also I think it's better to update the delegates during collection reload as you might not have the full view hierarchy yet when setting the delegate.

Finally, the class now recursively looks for UIScrollView so nothing is ever missed.

final class OrthogonalScrollingCollectionView: UICollectionView {
  override func reloadData() {
    super.reloadData()

    scrollViews(in: self).forEach { scrollView in
      scrollView.delegate = delegate
    }
  }

  override func reloadSections(_ sections: IndexSet) {
    super.reloadSections(sections)

    scrollViews(in: self).forEach { scrollView in
      scrollView.delegate = delegate
    }
  }

  fileprivate func scrollViews(in subview: UIView) -> [UIScrollView] {
    var scrollViews: [UIScrollView] = []
    subview.subviews.forEach { view in
      if let scrollView = view as? UIScrollView {
        scrollViews.append(scrollView)
      } else {
        scrollViews.append(contentsOf: self.scrollViews(in: view))
      }
    }
    return scrollViews
  }
}

Upvotes: 0

Alaeddine
Alaeddine

Reputation: 6212

You may just want to use visibleItemsInvalidationHandler callback of your NSCollectionLayoutSection it acts like the UIScrollViewDelegate it will be invoked each time the section scrolls

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered

section.visibleItemsInvalidationHandler = { (visibleItems, point, env) -> Void in
   print(point)
}

Upvotes: 13

Stoyan
Stoyan

Reputation: 1305

Setting orthogonalScrollingBehavior to a section, embeds an internal _UICollectionViewOrthogonalScrollerEmbeddedScrollView which handles the scrolling in a section. This internal scrollview is added as a subview to your collection view.

When you set yourself as a delegate to your collection view you should receive the scroll view delegate callbacks BUT ONLY for the main collection view, that scrolls between the sections and not the items in a section. Since the internal scrollviews (which may also be collectionViews, not sure) are completely different instances and you are not setting yourself as a delegate to them, you are not receiving their callbacks.

So as far as i know, there should not be an official way to receive these callbacks from the internal scrollviews that handle the scrolling in sections.

but if you are curious and you want to experiment with that you could use this 'hacked' collectionView class:

import UIKit

final class OrtogonalScrollingCollectionView: UICollectionView {

    override var delegate: UICollectionViewDelegate? {
        get { super.delegate }
        set {
            super.delegate = newValue
            subviews.forEach { (view) in
                guard String(describing: type(of: view)) == "_UICollectionViewOrthogonalScrollerEmbeddedScrollView" else { return }
                guard let scrollView = view as? UIScrollView else { return }
                scrollView.delegate = newValue
            }
        }
    }
}

that would set your delegate to all internal scrollview that come with the orthogonal sections. You should not be using this in production environment, because there is no guarantee that Apple will keep the inner workings of the collection views the same way so this hack may not work in the future, plus you might get rejected for using private APIs in UIKit when you submit a build for release.

Upvotes: 14

Related Questions