Skip to content

Instantly share code, notes, and snippets.

@arielm
Last active March 23, 2022 11:29
Show Gist options
  • Save arielm/9ec77960f2db4067b6c259e8fa9ea874 to your computer and use it in GitHub Desktop.
Save arielm/9ec77960f2db4067b6c259e8fa9ea874 to your computer and use it in GitHub Desktop.
Bug: Random "vertical jumps" while scrolling up in UICollectionViewController
import UIKit
class CustomCollectionViewCell: UICollectionViewCell {
private lazy var label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.font = UIFont.boldSystemFont(ofSize: 48)
view.textColor = .yellow
view.textAlignment = .center
return view
}()
private lazy var labelHeightConstraint: NSLayoutConstraint = {
return label.heightAnchor.constraint(equalToConstant: 0)
}()
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func initialize() {
contentView.backgroundColor = .red
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: contentView.topAnchor),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
labelHeightConstraint
])
}
func configure(index: Int, height: CGFloat) {
label.text = String(index)
labelHeightConstraint.constant = height
}
}
import UIKit
class CustomViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
enum Section {
case main
}
struct CustomItem: Hashable
{
let uuid = UUID()
static func ==(lhs: CustomItem, rhs: CustomItem) -> Bool {
return lhs.uuid == rhs.uuid
}
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
}
weak var delegate: ViewControllerChildDelegate?
private let layout: UICollectionViewCompositionalLayout = {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 20
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}()
private lazy var dataSource: UICollectionViewDiffableDataSource<Section, AnyHashable> = {
let dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in
let cell = self.cell(in: collectionView, for: indexPath, item: item)
return cell
}
return dataSource
}()
private let heights: [CGFloat] = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550]
init() {
super.init(collectionViewLayout: layout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .orange
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
update()
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
delegate?.childScrollViewWillBeginDragging(with: scrollView.contentOffset.y)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.childScrollViewDidScroll(to: scrollView.contentOffset.y)
}
private func cell(in collectionView: UICollectionView, for indexPath: IndexPath, item: AnyHashable) -> UICollectionViewCell {
switch item {
case is CustomItem:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCollectionViewCell
cell.configure(index: indexPath.row, height: heights[indexPath.row])
return cell
default:
return UICollectionViewCell()
}
}
private func update() {
var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
snapshot.appendSections([.main])
for _ in 0..<10 {
snapshot.appendItems([CustomItem()], toSection: .main)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}
import UIKit
protocol ViewControllerChildDelegate: AnyObject {
func childScrollViewWillBeginDragging(with offset: CGFloat)
func childScrollViewDidScroll(to offset: CGFloat)
}
class ViewController: UIViewController {
private lazy var pageViewController: UIPageViewController = {
let viewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
viewController.delegate = self
viewController.dataSource = self
return viewController
}()
private lazy var pagerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .brown
return view
}()
private let pagerViewHeight: CGFloat = 44
private var lastContentOffset: CGFloat = 0
lazy var pagerViewTopAnchorConstraint: NSLayoutConstraint = {
return pagerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "xxx"
view.addSubview(pagerView)
addChild(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pagerViewTopAnchorConstraint,
pagerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pagerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pagerView.heightAnchor.constraint(equalToConstant: pagerViewHeight),
pageViewController.view.topAnchor.constraint(equalTo: pagerView.bottomAnchor),
pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
let viewController = CustomViewController()
viewController.delegate = self
pageViewController.setViewControllers(
[viewController],
direction: .forward,
animated: false,
completion: nil
)
}
}
extension ViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
}
}
extension ViewController: ViewControllerChildDelegate {
func childScrollViewWillBeginDragging(with offset: CGFloat) {
lastContentOffset = offset
}
func childScrollViewDidScroll(to offset: CGFloat) {
if lastContentOffset > offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
} else if lastContentOffset < offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = -pagerViewHeight
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
}
}
}
@arielm
Copy link
Author

arielm commented Mar 20, 2022

ViewController should be embedded in a navigation-controller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment