37

While studying iOS6 new features I got a question about UICollectionView.
I am currently testing it with Flow layout and the scroll direction set to horizontal, scrolling and paging enabled. I've set its size to exactly the same as my custom's cells, so it can show one at a time, and by scrollig it sideways, the user would see the other existing cells.

It works perfectly.

Now I want to add and UIPageControl to the collection view I made, so it can show up which cell is visible and how many cells are there.

Building up the page control was rather simple, frame and numberOfPages defined.

The problem I am having, as the question titles marks, is how to get which cell is currently visible in the collection view, so it can change the currentPage of the page control.

I've tried delegate methods, like cellForItemAtIndexPath, but it is made to load cells, not show them. didEndDisplayingCell triggers when a cell its not displayed anymore, the opposite event of what I need.

Its seems that -visibleCells and -indexPathsForVisibleItems, collection view methods, are the correct choice for me, but I bumped into another problem. When to trigger them?

Thanks in advance, hope I made myself clear enough so you guys can understand me!

gazzola
  • 380
  • 1
  • 4
  • 14

8 Answers8

92

You must setup yourself as UIScrollViewDelegate and implement the scrollViewDidEndDecelerating:method like so:

Objective-C

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat pageWidth = self.collectionView.frame.size.width;
    self.pageControl.currentPage = self.collectionView.contentOffset.x / pageWidth;
}

Swift

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

    let pageWidth = self.collectionView.frame.size.width
    pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
}
Avario
  • 4,655
  • 3
  • 26
  • 19
Sendoa
  • 4,705
  • 5
  • 27
  • 21
  • 2
    Without having to create an IVAR for CollectionView: `- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { CGFloat pageWidth = _tutorialCollectionViewFrame.size.width; _pageControl.currentPage = scrollView.contentOffset.x / pageWidth; }` – mmackh May 18 '13 at 09:03
  • 1
    Yeah that is correct answer...: func scrollViewDidEndDecelerating(scrollView: UIScrollView) { let pageWidth: CGFloat = self.collectionView.frame.size.width self.pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth) } – Thiha Aung May 20 '16 at 10:45
  • I found it smoother to use scrollViewDidScroll instead because you could theoretically scroll side to side without the pageControl updating until it stops decelerating. My code looks like this: func scrollViewDidScroll(_ scrollView: UIScrollView) { let page = Int(round(self.collectionView.contentOffset.x / self.collectionView.frame.size.width)) if self.pageControl.currentPage != page { self.pageControl.currentPage = page } } – hordurh Aug 14 '17 at 14:13
18

I struggled with this for a while as well, then I was advised to check out the parent classes of UICollectionView. One of them happens to be UIScrollView and if you set yourself up as a UIScrollViewDelegate, you get access to very helpful methods such as scrollViewDidEndDecelerating, a great place to update the UIPageControl.

Not Randy Marsh
  • 414
  • 4
  • 4
17

I would recommend a little tuned calculation and handling as it will update page control immediately in any scroll position with better accuracy.

The solution below works with any scroll view or it subclass (UITableView UICollectionView and others)

in viewDidLoad method write this

scrollView.delegate = self

then use code for your language:

Swift 3

func scrollViewDidScroll(_ scrollView: UIScrollView) 
    {
       let pageWidth = scrollView.frame.width
       pageControl.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
    }

Swift 2:

func scrollViewDidScroll(scrollView: UIScrollView) 
{
   let pageWidth = CGRectGetWidth(scrollView.frame)
   pageControl.currentPage = Int((scrollView.contentOffset.x + pageWidth / 2) / pageWidth)
}

Objective C

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat pageWidth = self.collectionView.frame.size.width;
    self.pageControl.currentPage = (self.collectionView.contentOffset.x + pageWidth / 2) / pageWidth;
}
Nikolay Shubenkov
  • 3,133
  • 1
  • 29
  • 31
  • Your calculations is better, but `-[scrollViewDidEndDecelerating:]` is better place for this code. – k06a Nov 18 '14 at 14:54
  • It's depends on your taste I think. Apple updates pageControl after scroll is finished, but for me updating of page control more often looks better for user. If it is not slow down performance you can use this method. – Nikolay Shubenkov Nov 24 '14 at 11:12
  • I saw not nice blinking while tapping on left and right parts of `UIPageControl` in this case. I mean change page by `UIPageControl` tapping. – k06a Nov 24 '14 at 11:15
8

Another option with less code is to use visible item index path and set the page control.

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    self.pageControl.currentPage = [[[self.collectionView indexPathsForVisibleItems] firstObject] row];
}
Ramesh
  • 1,703
  • 18
  • 13
5
  1. Place PageControl in your view or set by Code.
  2. Set UIScrollViewDelegate
  3. In Collectionview-> cellForItemAtIndexPath (Method) add the below code for calculate the Number of pages,

int pages =floor(ImageCollectionView.contentSize.width/ImageCollectionView.frame.size.width); [pageControl setNumberOfPages:pages];

Add the ScrollView Delegate method,

pragma mark - UIScrollVewDelegate for UIPageControl

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat pageWidth = ImageCollectionView.frame.size.width;
    float currentPage = ImageCollectionView.contentOffset.x / pageWidth;

    if (0.0f != fmodf(currentPage, 1.0f))
    {
        pageControl.currentPage = currentPage + 1;
    }
    else
    {
        pageControl.currentPage = currentPage;
    }
    NSLog(@"finishPage: %ld", (long)pageControl.currentPage);
}
Ramdhas
  • 1,765
  • 1
  • 18
  • 26
4

I know this is an old one but I've just needed to implement this sort of feature again and have a bit to add which gives a more complete answer.

Firstly: Using scrollViewDidEndDecelerating assumes that the user lifted their finger while dragging (more like a flick action) and therefore there is a deceleration phase. If the user drags without lifting the finger the UIPageControl will still indicate the old page from before the drag began. Instead using the scrollViewDidScroll callback means that the view is updated both after dragging and flicking and also during dragging and scrolling so it feels much more responsive and accurate for the user.

Secondly: Relying on the pagewidth for calculating the selected index assumes all the cells have the same width and that there is one cell per screen. taking advantage of the indexPathForItemAtPoint method on UICollectionView gives a more resilient result which will work for different layouts and cell sizes. The implementation below assumes the centre of the frame is the desired cell to be represented in the pagecontrol. Also if there are intercell spacings there will times during scrolling when the selectedIndex could be nil or optional so this needs to be checked and unwrapped before setting on the pageControl.

 func scrollViewDidScroll(scrollView: UIScrollView) {
   let contentOffset = scrollView.contentOffset
   let centrePoint =  CGPointMake(
    contentOffset.x + CGRectGetMidX(scrollView.frame),
    contentOffset.y + CGRectGetMidY(scrollView.frame)
   )
   if let index = self.collectionView.indexPathForItemAtPoint(centrePoint){
     self.pageControl.currentPage = index.row
   }
 }

One more thing - set the number of pages on the UIPageControl with something like this:

  func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    self.pageControl.numberOfPages = 20
    return self.pageControl.numberOfPages
  } 
Dallas Johnson
  • 1,546
  • 10
  • 13
3

Simple Swift

public func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    pageControl.currentPage = (collectionView.indexPathsForVisibleItems().first?.row)!
}

UIScrollViewDelegate is already implemented if you implement UICollectionViewDelegate

Michael
  • 9,639
  • 3
  • 64
  • 69
0

If using scrollViewDidScroll, updating the page control should be done manually to ⚠️ avoid the flickering dots when you tap on the page control.

Setup the UIPageControl.

let pageControl = UIPageControl()
pageControl.pageIndicatorTintColor = .label
pageControl.defersCurrentPageDisplay = true // Opt-out from automatic display
pageControl.numberOfPages = viewModel.items.count
pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)

Implement the action (using the extensions below).

@objc func pageControlValueChanged(_ sender: UIPageControl) {
    collectionView.scroll(to: sender.currentPage)
}

Update UIPageControl manually on every scroll.

extension ViewController: UIScrollViewDelegate {
        
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        pageControl.currentPage = collectionView.currentPage
        pageControl.updateCurrentPageDisplay() // Display only here
    }
}

Convinient UICollectionView extensions.

extension CGRect {
    
    var middle: CGPoint {
        CGPoint(x: midX, y: midY)
    }
}

extension UICollectionView {
    
    var visibleArea: CGRect {
        CGRect(origin: contentOffset, size: bounds.size)
    }
    
    var currentPage: Int {
        indexPathForItem(at: visibleArea.middle)?.row ?? 0
    }
    
    func scroll(to page: Int) {
        scrollToItem(
            at: IndexPath(row: page, section: 0),
            at: .centeredHorizontally,
            animated: true
        )
    }
}
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172