33
votes

I'm using a UICollectionView with a flow layout to show a list of cells, I also have a page control to indicate current page, but there seems to be no way to get current index path, I know I can get visible cells:

UICollectionView current visible cell index

however there can be more than one visible cells, even if each of my cells occupies full width of the screen, if I scroll it to have two halves of two cells, then they are both visible, so is there a way to get only one current visible cell's index?

Thanks

11

11 Answers

60
votes

You can get the current index by monitoring contentOffset in scrollViewDidScroll delegate

it will be something like this

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSInteger currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;

}
32
votes

Get page via NSIndexPath from center of view.

Works even your page not equal to width of UICollectionView.

    func scrollViewDidScroll(scrollView: UIScrollView) {
    let center = CGPoint(x: scrollView.contentOffset.x + (scrollView.frame.width / 2), y: (scrollView.frame.height / 2))
    if let ip = collectionView.indexPathForItemAtPoint(center) {
        self.pageControl.currentPage = ip.row
    }
}
14
votes

Definitely you need catch the visible item when the scroll movement is stopped. Use next code to do it.

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    if let indexPath = myCollectionView.indexPathsForVisibleItems.first {
        myPageControl.currentPage = indexPath.row
    }
}
9
votes

Swift 5.1

The easy way and more safety from nil crash

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if collectionView == newsCollectionView {
        if newsPager.currentPage == indexPath.row {
            guard let visible = newsCollectionView.visibleCells.first else { return }
            guard let index = newsCollectionView.indexPath(for: visible)?.row else { return }
            newsPager.currentPage = index
        }

    }
}
7
votes
  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];
    
  4. Add the ScrollView Delegate method,

    #pragma mark - UIScrollViewDelegate 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);
    }
    
3
votes

I had similar situation where my flow layout was set for UICollectionViewScrollDirectionHorizontal and I was using page control to show the current page.

I achieved it using custom flow layout.

/------------------------ Header file (.h) for custom header ------------------------/

/**
* The customViewFlowLayoutDelegate protocol defines methods that let you coordinate with
*location of cell which is centered.
*/

@protocol CustomViewFlowLayoutDelegate <UICollectionViewDelegateFlowLayout>

/** Informs delegate about location of centered cell in grid.
*  Delegate should use this location 'indexPath' information to 
*   adjust it's conten associated with this cell. 
*   @param indexpath of cell in collection view which is centered.
*/

- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath;
@end

@interface customViewFlowLayout : UICollectionViewFlowLayout
@property (nonatomic, weak) id<CustomViewFlowLayoutDelegate> delegate;
@end

/------------------- Implementation file (.m) for custom header -------------------/

@implementation customViewFlowLayout
- (void)prepareLayout {
 [super prepareLayout];
 }

static const CGFloat ACTIVE_DISTANCE = 10.0f; //Distance of given cell from center of visible rect
 static const CGFloat ITEM_SIZE = 40.0f; // Width/Height of cell.

- (id)init {
    if (self = [super init]) {
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.minimumInteritemSpacing = 60.0f;
    self.sectionInset = UIEdgeInsetsZero;
    self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
    self.minimumLineSpacing = 0;
}
    return self;
    }

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
   NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;

for (UICollectionViewLayoutAttributes *attribute in attributes) {
    if (CGRectIntersectsRect(attribute.frame, rect)) {

        CGFloat distance = CGRectGetMidX(visibleRect) - attribute.center.x;
        // Make sure given cell is center
        if (ABS(distance) < ACTIVE_DISTANCE) {
            [self.delegate collectionView:self.collectionView layout:self cellCenteredAtIndexPath:attribute.indexPath];
        }
    }
}
return attributes;
}

Your class containing collection view must conform to protocol 'CustomViewFlowLayoutDelegate' I described earlier in custom layout header file. Like:

@interface MyCollectionViewController () <UICollectionViewDataSource, UICollectionViewDelegate, CustomViewFlowLayoutDelegate>
@property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
@property (strong, nonatomic) IBOutlet UIPageControl *pageControl;
....
....
@end

There are two ways to hook your custom layout to collection view, either in xib OR in code like say in viewDidLoad:

customViewFlowLayout *flowLayout = [[customViewFlowLayout alloc]init];
flowLayout.delegate = self;
self.collectionView.collectionViewLayout = flowLayout;
self.collectionView.pagingEnabled = YES; //Matching your situation probably?

Last thing, in MyCollectionViewController implementation file, implement delegate method of 'CustomViewFlowLayoutDelegate'.

- (void)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout cellCenteredAtIndexPath:(NSIndexPath *)indexPath {
self.pageControl.currentPage = indexPath.row;

}

I hope this would be helpful. :)

1
votes

for swift 4.2

@IBOutlet weak var mPageControl: UIPageControl!
@IBOutlet weak var mCollectionSlider: UICollectionView!

private var _currentIndex = 0
private var T1:Timer!
private var _indexPath:IndexPath = [0,0]

private func _GenerateNextPage(){
    self._currentIndex = mCollectionSlider.indexPathForItem(at: CGPoint.init(x: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midX, y: CGRect.init(origin: mCollectionSlider.contentOffset, size: mCollectionSlider.bounds.size).midY))?.item ?? 0
    self.mPageControl.currentPage = self._currentIndex
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    _SetTimer(AutoScrollInterval)
    _GenerateNextPage()
}

@objc private func _AutoScroll(){
    self._indexPath = IndexPath.init(item: self._currentIndex+1, section: 0)
    if !(self._indexPath.item < self.numberOfItems){
        _indexPath = [0,0]
    }
    self.mCollectionSlider.scrollToItem(at: self._indexPath, at: .centeredHorizontally, animated: true)
}
private func _SetTimer(_ interval:TimeInterval){
    if T1 == nil{
        T1 = Timer.scheduledTimer(timeInterval: interval , target:self , selector: #selector(_AutoScroll), userInfo: nil, repeats: true)
    }
}

you can skip the function _SetTimer() , thats for auto scroll

1
votes

Note - I have found andykkt's answer useful but since it is in obj-c converted it to swift and also implemented logic in another UIScrollView delegate for a smoother effect.

func updatePageNumber() {
    // If not case to `Int` will give an error.
    let currentPage = Int(ceil(scrollView.contentOffset.x / scrollView.frame.size.width))
    pageControl.currentPage = currentPage
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    // This will be call when you scrolls it manually.
    updatePageNumber()
}

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
    // This will be call when you scrolls it programmatically.
    updatePageNumber()
}
1
votes

With UICollectionViewDelegate methods

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    pageControl.currentPage = indexPath.row
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if pageControl.currentPage == indexPath.row {
        pageControl.currentPage = collectionView.indexPath(for: collectionView.visibleCells.first!)!.row
    }
}
0
votes
(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat pageWidth = _cvImagesList.frame.size.width;
    float currentPage = _cvImagesList.contentOffset.x / pageWidth;

     _pageControl.currentPage = currentPage + 1;
    NSLog(@"finishPage: %ld", (long)_pageControl.currentPage);
}
0
votes

Swift 5.0


 extension youriewControllerName:UIScrollViewDelegate{
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    
            let pageWidth = self.collectionView.frame.size.width
            pageControl.currentPage = Int(self.collectionView.contentOffset.x / pageWidth)
        }
    }