Search code examples
iosswiftuicollectionviewuicollectionviewcell

Infinite horizontal collectionView with first cell able to scroll left


I created an infinite horizontal collectionView presenting the first 6 items of my database, something like carousel. So, when I am scrolling to the right side, I can see those 6 items with the order I got them in that way:

1-2-3-4-5-6-1-2-3-4-5-6-1-2... etc. (bold 1 represents the first cell of collection view)

What I need to do now is when the collection view is rendered and I can see the first item, if I scroll left I want to present the last item I got from database (the 6th item).

...1-2-3-4-5-6-1-2-3-4-5-6-1-2-3-4-5-6-1-2... etc. (again, bold 1 represents the first cell of collection view)

Do you know how I can implement this feature?


Solution

  • There are various different ways to accomplish this... which method to use would depend on a number of things related to your UI layout.

    But, assuming we do want to use a horizontal-scrolling collection view, and, based on your comments you want paging with two cells per page...

    Let's say we have a simple string array for our data:

    let myData: [String] = [
        "1", "2", "3", "4", "5", "6",
    ]
    

    If we have a simple single-label collection view cell, our cellForItemAt could look like this:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SimpleCell.identifier, for: indexPath) as! SimpleCell
        
        let thisData = myData[indexPath.item % myData.count]
        cell.theLabel.text = thisData
        
        return cell
        
    }
    

    That gives us the "carousel" effect.

    To allow the user to scroll right-to-left immediately, we cannot start at item: 0.

    So, we can set the number of items to some absurdly large number that nobody will ever scroll to, such as:

    lazy var totalCells: Int = myData.count * 100_000
    

    Then (I'm assuming) in viewDidLayoutSubviews() - where we know the width of the collection view - we're setting the flow layout's .itemSize ... and we can scroll the collection view to totalCells / 2.

    Might look something like this:

    var cvWidth: CGFloat = -1.0
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print(#function)
        if cvWidth != myCollectionView.frame.size.width {
            cvWidth = myCollectionView.frame.size.width
            if let fl = myCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
                let h = fl.itemSize.height
                let w = (cvWidth - fl.minimumInteritemSpacing) * 0.5
                fl.itemSize = .init(width: w, height: h)
            }
            
            // start with collection view "scrolled half-way"
            //  through the absurdly large number of items
            let startItem: Int = totalCells / 2
            myCollectionView.scrollToItem(at: IndexPath(item: startItem, section: 0), at: .left, animated: false)
            
            // enable paging here
            myCollectionView.isPagingEnabled = true
        }
    }
    

    Now when the user first sees the collection view, it will already be scrolled to item 300_000, and we can scroll both left and right.

    Here's a quick, complete example...

    simple single-label cell

    class SimpleCell: UICollectionViewCell {
        static let identifier: String = "simpleCell"
        
        public var theLabel: UILabel = {
            let label = UILabel()
            label.font = .systemFont(ofSize: 14, weight: .bold)
            label.backgroundColor = .systemBlue
            label.textColor = .white
            label.textAlignment = .center
            return label
        }()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        private func commonInit() {
            
            theLabel.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(theLabel)
            
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                theLabel.topAnchor.constraint(equalTo: g.topAnchor),
                theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            ])
            
            // so we can see the cell framing
            contentView.backgroundColor = .yellow
            contentView.layer.borderColor = UIColor.red.cgColor
            contentView.layer.borderWidth = 1
            
        }
    }
    

    example view controller with collection view

    class SimpleExampleVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
        
        var myCollectionView: UICollectionView!
        
        let myData: [String] = [
            "1", "2", "3", "4", "5", "6",
        ]
        
        // some absurdly large number that nobody will ever scroll to
        lazy var totalCells: Int = myData.count * 100_000
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let fl = UICollectionViewFlowLayout()
            fl.scrollDirection = .horizontal
            fl.minimumLineSpacing = 0.0
            fl.minimumInteritemSpacing = 0.0
            
            // .itemSize width will be changed in viewDidLayoutSubviews()
            fl.itemSize = .init(width: 120.0, height: 60.0)
            
            myCollectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
            
            myCollectionView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(myCollectionView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                myCollectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                myCollectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                myCollectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                myCollectionView.heightAnchor.constraint(equalToConstant: 64.0),
            ])
            
            myCollectionView.register(SimpleCell.self, forCellWithReuseIdentifier: SimpleCell.identifier)
            myCollectionView.dataSource = self
            myCollectionView.delegate = self
    
            // paging should be disabled at this point
            //  we don't have to worry if we're creating the collection view
            //  via code, as we're doing here... but...
            //  if we're using Storyboard, we want to make sure
            //  paging is NOT set to true
            myCollectionView.isPagingEnabled = false
    
            // just so we can see the framing
            myCollectionView.backgroundColor = .darkGray
        }
        
        var cvWidth: CGFloat = -1.0
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            print(#function)
            if cvWidth != myCollectionView.frame.size.width {
                cvWidth = myCollectionView.frame.size.width
                if let fl = myCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
                    let h = fl.itemSize.height
                    let w = cvWidth * 0.5
                    fl.itemSize = .init(width: w, height: h)
                }
                
                // start with collection view "scrolled half-way"
                //  through the absurdly large number of items
                let startItem: Int = totalCells / 2
                myCollectionView.scrollToItem(at: IndexPath(item: startItem, section: 0), at: .left, animated: false)
                
                // enable paging here
                myCollectionView.isPagingEnabled = true
            }
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return totalCells
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SimpleCell.identifier, for: indexPath) as! SimpleCell
            
            let thisData = myData[indexPath.item % myData.count]
            cell.theLabel.text = thisData
            
            return cell
            
        }
        
    }