Search code examples
swiftuicollectionviewswift4infinite-scrolluipagecontrol

Infinite UICollectionview with UIPageControl


In below given code I can simply infinitely scroll through the cells. The page control also seems to be working at least while scrolling forward (to the right of screen). But frankly, when I scroll to the left, I see weird index path jump of 3 items (by the way my data source has 3 items for test purposes). In the screen I notice nothing wrong. But page control seems to be freezing for a moment and starts to work again but with a shift and shows wrong dot for the cell. Any Ideas? Thanks for any help...

import UIKit

class HomeHeaderCell: CategoryCell {

    private let cellId = "cellId"
    private var timer: Timer?
    private let infiniteSize = 1000
    private var onlyOnce = true

    // ...

    let pageControl: UIPageControl = {
        let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 50))
        let pc = UIPageControl(frame: rect)
        pc.currentPage = 0
        pc.numberOfPages = 3
        pc.pageIndicatorTintColor = .gray
        pc.currentPageIndicatorTintColor = .red
        pc.isUserInteractionEnabled = false
        return pc
    }()

    override func setupViews() {

        addSubview(baseCollectionView)
        addSubview(pageControl)


        baseCollectionView.delegate = self
        baseCollectionView.dataSource = self
        baseCollectionView.register(HeaderCell.self, forCellWithReuseIdentifier: cellId)

        baseCollectionView.backgroundColor = .white
        baseCollectionView.isPagingEnabled = true
        baseCollectionView.isScrollEnabled = true

        baseCollectionView.showsHorizontalScrollIndicator = false


        baseCollectionView.anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
        pageControl.anchor(top: nil, leading: leadingAnchor, bottom: bottomAnchor, trailing: nil, padding: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 0))
    }


    @objc func autoScroll() {
        guard let currentItemNumber = baseCollectionView.indexPathsForVisibleItems.first?.item  else { return }
        let nextItemNumber = currentItemNumber + 1
        let nextIndexPath = IndexPath(item: nextItemNumber, section: 0)
        baseCollectionView.scrollToItem(at: nextIndexPath, at: .left, animated: true)
    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        stopTimer()
    }
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        startTimer()
    }

    func startTimer() {
        timer = Timer.scheduledTimer(timeInterval: 4.0, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
    }

    func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return infiniteSize
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! HeaderCell

        // Maybe can be done more elegantly...
        if let foods = foodCategory?.foods {
            let numberOfFood = indexPath.item % foods.count
            cell.food = foods[numberOfFood]
            pageControl.currentPage = numberOfFood
            print(numberOfFood)
        }
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if onlyOnce  {
            let middleIndex = IndexPath(item: Int (infiniteSize / 2), section: 0)
            baseCollectionView.scrollToItem(at: middleIndex, at: .centeredHorizontally, animated: false)
            startTimer()
            onlyOnce = false
        }
    }

    override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: frame.width, height: frame.height)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    private class HeaderCell: ItemCell {

        override func setupViews() {
            addSubview(imageView)

            imageView.layer.cornerRadius = 0
            imageView.layer.borderColor = UIColor(white: 0.5, alpha: 0.5).cgColor
            imageView.layer.borderWidth = 0.5

            imageView.anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
        }
    }

}

Solution

  • I solved my own question. Here is the code you should be applying. First two methods are UIScrollView methods that comes free with UICollectionView, which by the way a subclass of UIScrollView. updatePageControl method is the one that I wrote just to keep code clean and simple. Hope this helps...

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        updatePageControl(scrollView: scrollView)
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updatePageControl(scrollView: scrollView)
    }
    func updatePageControl(scrollView: UIScrollView) {
        let pageNumber = round(scrollView.contentOffset.x / scrollView.bounds.size.width)
        guard let count = foodCategory?.foods?.count else {return}
        let currentPageNumber = Int(pageNumber) % count
        pageControl.currentPage = currentPageNumber
    }