So I have a weird problem:
I'm building infinite carausel (UICollectionView) that needs to be in vertical UIScrollView.
When I try to embed UICollectionView with a horizontal scroll into a vertical scrollview when I start scrolling vertically, UICollectionView dissapears. This is simplified code of my component:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var scrollView: UIScrollView!
private var contentView: UIStackView!
private var collectionView: UICollectionView!
private var items = [UIColor.red, UIColor.green]
private var currentIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
setupCollectionView()
}
private func setupScrollView() {
scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = true
scrollView.alwaysBounceVertical = true
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView = UIStackView()
contentView.axis = .vertical
contentView.alignment = .center
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalToSuperview()
make.height.greaterThanOrEqualToSuperview()
}
}
private func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.isPagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
collectionView.isMultipleTouchEnabled = true
contentView.addArrangedSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(200)
}
contentView.addArrangedSubview(horizontalStackView)
horizontalStackView.snp.makeConstraints { make in
make.top.equalTo(collectionView.snp.bottom).offset(20)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-20) // Ensure the stack view is within scroll view bounds
}
contentView.addArrangedSubview(UIView())
// Initialize starting position
DispatchQueue.main.async {
self.collectionView.scrollToItem(at: IndexPath(item: self.items.count * 10, section: 0), at: .centeredHorizontally, animated: false)
}
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count * 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselCell.reuseIdentifier, for: indexPath) as! CarouselCell
cell.cardView.backgroundColor = items[indexPath.item % items.count]
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.bounds.size
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard scrollView = collectionView as? UICollectionView else { return }
let pageWidth = scrollView.frame.width
let currentPage = scrollView.contentOffset.x / pageWidth
let numberOfItems = CGFloat(items.count * 20)
let pageIndex = Int(currentPage) % items.count
currentIndex = pageIndex
if currentPage < 1 {
scrollView.setContentOffset(CGPoint(x: pageWidth * (numberOfItems + currentPage), y: 0), animated: false)
} else if currentPage >= numberOfItems {
scrollView.setContentOffset(CGPoint(x: pageWidth * (currentPage - numberOfItems), y: 0), animated: false)
}
}
}
// Custom UICollectionViewCell
class CarouselCell: UICollectionViewCell {
static let reuseIdentifier = "CarouselCell"
let cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 12
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
contentView.addSubview(cardView)
// Add padding and set constraints for the cardView
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10)
])
}
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Add any additional logic for the vertical scroll view if needed
}
}
Everything works fine if I remove scrollView but still I need its functionality. Please help.
The collection view is "disappearing" because you are setting its .contentOffset
based on the scrollView
...
Change your scrollViewDidEndDecelerating
to only manipulate the collection view if it is triggering that delegate function:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == collectionView {
let pageWidth = scrollView.frame.width
let currentPage = scrollView.contentOffset.x / pageWidth
let numberOfItems = CGFloat(items.count * 20)
let pageIndex = Int(currentPage) % items.count
currentIndex = pageIndex
if currentPage < 1 {
scrollView.setContentOffset(CGPoint(x: pageWidth * (numberOfItems + currentPage), y: 0), animated: false)
} else if currentPage >= numberOfItems {
scrollView.setContentOffset(CGPoint(x: pageWidth * (currentPage - numberOfItems), y: 0), animated: false)
}
}
}
Edit
The change you made:
guard scrollView = collectionView as? UICollectionView else { return }
is attempting to assign collectionView
to scrollView
, instead of comparing them.
If you want to use a guard
instead of an if
, it should be this:
guard scrollView == collectionView else { return }
Here is your complete code. Take a good look at the changes I made to your SnapKit constraints... you were doing a number of things incorrectly:
import UIKit
import SnapKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var scrollView: UIScrollView!
private var contentView: UIStackView!
private var collectionView: UICollectionView!
private var items = [UIColor.red, UIColor.green]
private var currentIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
setupCollectionView()
// let's use some background colors, so we can
// see the frames of our UI elements
view.backgroundColor = .systemYellow
scrollView.backgroundColor = .systemBlue
contentView.backgroundColor = .cyan
collectionView.backgroundColor = .yellow
}
private func setupScrollView() {
scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = true
scrollView.alwaysBounceVertical = true
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
contentView = UIStackView()
contentView.axis = .vertical
contentView.alignment = .center
contentView.spacing = 12.0
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.top.leading.trailing.bottom.equalTo(scrollView.contentLayoutGuide)
make.width.equalTo(scrollView.frameLayoutGuide)
}
}
private func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.isPagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
collectionView.isMultipleTouchEnabled = true
contentView.addArrangedSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.width.equalToSuperview()
make.height.equalTo(200)
}
let horizontalStackView = UIStackView()
horizontalStackView.distribution = .equalSpacing
contentView.addArrangedSubview(horizontalStackView)
horizontalStackView.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.8)
}
for i in 0..<4 {
if let img = UIImage(systemName: "\(i).square.fill") {
let v = UIImageView(image: img)
v.tintColor = .systemGreen
horizontalStackView.addArrangedSubview(v)
v.snp.makeConstraints { make in
make.height.width.equalTo(40.0)
}
}
}
// let's add a bunch of views to the content stack view,
// so we will have vertical scrolling
for _ in 0..<8 {
let v = UIView()
v.backgroundColor = .systemBrown
contentView.addArrangedSubview(v)
v.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.9)
make.height.equalTo(120.0)
}
}
// Initialize starting position
DispatchQueue.main.async {
self.collectionView.scrollToItem(at: IndexPath(item: self.items.count * 10, section: 0), at: .centeredHorizontally, animated: false)
}
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count * 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselCell.reuseIdentifier, for: indexPath) as! CarouselCell
cell.cardView.backgroundColor = items[indexPath.item % items.count]
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return collectionView.bounds.size
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard scrollView == collectionView else { return }
let pageWidth = scrollView.frame.width
let currentPage = scrollView.contentOffset.x / pageWidth
let numberOfItems = CGFloat(items.count * 20)
let pageIndex = Int(currentPage) % items.count
currentIndex = pageIndex
if currentPage < 1 {
scrollView.setContentOffset(CGPoint(x: pageWidth * (numberOfItems + currentPage), y: 0), animated: false)
} else if currentPage >= numberOfItems {
scrollView.setContentOffset(CGPoint(x: pageWidth * (currentPage - numberOfItems), y: 0), animated: false)
}
}
}
// Custom UICollectionViewCell
class CarouselCell: UICollectionViewCell {
static let reuseIdentifier = "CarouselCell"
let cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 12
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
contentView.addSubview(cardView)
// Add padding and set constraints for the cardView
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10)
])
}
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Add any additional logic for the vertical scroll view if needed
}
}