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?
CodePudding user response:
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
CodePudding user response:
You can set tag for each collectionView and check tag of scrollView in scrollViewDidScroll.