iosswiftuicollectionviewuiscrollviewuikit

UICollectionView dissapears when embedded into UIScrollView


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.


Solution

  • 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
        }
    }