Last active
March 23, 2022 11:29
-
-
Save arielm/9ec77960f2db4067b6c259e8fa9ea874 to your computer and use it in GitHub Desktop.
Bug: Random "vertical jumps" while scrolling up in UICollectionViewController
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ViewController should be embedded in a navigation-controller.