skip to Main Content

I have created a User Onboarding as a Collection View with 5 cells (pages).

The Collection View has a UIPageControl which shows an active page user currently on and 2 UIButtons (previous and next) which needed to manually scroll the pages if user don’t want to swipe.

Here is how I manage the buttons IBAction when user taps:

@IBAction func prevButtonClicked(_ sender: UIButton) {
    if currentPage != 0 {
        currentPage -= 1
        let indexPath = IndexPath(item: currentPage, section: 0)
        collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    }
}

@IBAction func nextButtonClicked(_ sender: UIButton) {
    if currentPage == slides.count - 1 {
        //hide onboarding
    } else {
        currentPage += 1
        let indexPath = IndexPath(item: currentPage, section: 0)
        collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    }
}

Also if user swipes a page instead of tap on buttons I use scrollViewDidScroll() method to update UIPageControl dot:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let visibleRectangle = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
    let visiblePoint = CGPoint(x: visibleRectangle.midX, y: visibleRectangle.midY)
    currentPage = collectionView.indexPathForItem(at: visiblePoint)?.row ?? 0
}

The currentPage is a computed property:

private var currentPage = 0 {
    didSet {
        pageControl.currentPage = currentPage
        currentPage == 0 ? hidePreviousButton() : showPreviousButton()
    }
}

I have a problem: when tap on buttons I force collectionView to scroll and update currentPage, therefore scrollViewDidScroll called and currentPage updates again.

Because of that when I tap on buttons I can see that UIPageControl dot and backButton are flicker since the code runs twice:

    didSet {
        pageControl.currentPage = currentPage
        currentPage == 0 ? hidePreviousButton() : showPreviousButton()
    }

Here is a GIF with the problem: GIF

How can I avoid the double call to scrollViewDidScroll when tap on buttons?

2

Answers


  1. You can set tag for each collectionView and check tag of scrollView in scrollViewDidScroll.

    Login or Signup to reply.
  2. Add a Bool var to your OnboardingViewController:

    var programmedScroll: Bool = false
    

    then, when prev or next button is tapped, instead of:

    @IBAction func prevButtonPressed(_ sender: UIButton) {
        if currentPage != 0 {
            currentPage -= 1
            let indexPath = IndexPath(item: currentPage, section: 0)
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
        }
    }
    

    do this:

    @IBAction func prevButtonPressed(_ sender: UIButton) {
        if currentPage != 0 {
            currentPage -= 1
            let indexPath = IndexPath(item: currentPage, section: 0)
            // instead of this
            //collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            self.programmedScroll = true
            UIView.animate(withDuration: 0.3, animations: {
                self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false)
            }, completion: { _ in
                self.programmedScroll = false
            })
        }
    }
    

    Now your scrollViewDidScroll won’t be called during that animation.


    Edit

    In scrollViewDidScroll implementation:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if !programmedScroll {
            let visibleRectangle = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
            let visiblePoint = CGPoint(x: visibleRectangle.midX, y: visibleRectangle.midY)
            currentPage = collectionView.indexPathForItem(at: visiblePoint)?.row ?? 0
        }
    }
    

    Edit 2

    Using the above approach resulted in a less-than-acceptable scroll effect, because a UICollectionView only renders cells that will be displayed.

    When telling the collection view to .scrollToItem with animated: false, the collection view immediately drops the rendering of the cell that will no longer be visible.

    So, we’ll take the same approach, but find another way to "re-enable" the scrollViewDidScroll code after a Next / Prev button has called .scrollToItem.

    In prev/next, let’s still set self.programmedScroll = true, but instead of the animation block let’s use the built-in animation:

    @IBAction func prevButtonPressed(_ sender: UIButton) {
        if currentPage != 0 {
            currentPage -= 1
            let indexPath = IndexPath(item: currentPage, section: 0)
            // disable scrollViewDidScroll code execution
            self.programmedScroll = true
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
        }
    }
    
    @IBAction func nextButtonPressed(_ sender: UIButton) {
        if currentPage == slides.count - 1 {
            //hide onboarding
        } else {
            currentPage += 1
            let indexPath = IndexPath(item: currentPage, section: 0)
            // disable scrollViewDidScroll code execution
            self.programmedScroll = true
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
        }
    }
    

    then we need to "re-enable" the code to change the page control dot mid-way between cells when dragging, so we’ll implement:

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        // re-enable execution of scrollViewDidScroll code
        programmedScroll = false
    }
    

    That should do it. I updated the repo at: https://github.com/DonMag/TestCollectionViewOnboarding

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search