125

I am using UICollectionView first time in my iPad application. I have set UICollectionView such that its size and cell size is same, means only once cell is displayed at a time.

Problem: Now when user scroll UICollectionView I need to know which cell is visible I have to update other UI elements on change. I didn't find any delegate method for this. How can I achieve this?

Code:

[self.mainImageCollection setTag:MAIN_IMAGE_COLLECTION_VIEW];
[self.mainImageCollection registerClass:[InspirationMainImageCollectionCell class] forCellWithReuseIdentifier:@"cellIdentifier"];
[self.mainImageFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[self.mainImageFlowLayout setMinimumInteritemSpacing:0.0f];
[self.mainImageFlowLayout setMinimumLineSpacing:0.0f];
self.mainImageFlowLayout.minimumLineSpacing = 0;
[self.mainImageCollection setPagingEnabled:YES];
[self.mainImageCollection setShowsHorizontalScrollIndicator:NO];
[self.mainImageCollection setCollectionViewLayout:self.mainImageFlowLayout];

What I have tried:

As UICollectionView conforms to UIScrollView, I got when user scroll ends with UIScrollViewDelegate method

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

But inside above function how can I get current visible cell index of UICollectionView ?

Marwen Doukh
  • 1,745
  • 13
  • 23
Irfan DANISH
  • 7,467
  • 10
  • 35
  • 63

16 Answers16

193

indexPathsForVisibleItems might work for most situations, but sometimes it returns an array with more than one index path and it can be tricky figuring out the one you want. In those situations, you can do something like this:

CGRect visibleRect = (CGRect){.origin = self.collectionView.contentOffset, .size = self.collectionView.bounds.size};
CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
NSIndexPath *visibleIndexPath = [self.collectionView indexPathForItemAtPoint:visiblePoint];

This works especially well when each item in your collection view takes up the whole screen.

Swift version

let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint)
Arthur
  • 264
  • 2
  • 11
lobianco
  • 5,770
  • 2
  • 30
  • 45
  • 9
    I can agree, sometimes indexPathsForVisibleItems returns more cells, even if we think there shouldn't be such case. Your solution works great. – MP23 Sep 17 '14 at 15:07
  • 2
    I was totally on this kind of situation, just one fix that worked for me (but my case was with tableview), I needed to change the CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect)); for CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMinY(visibleRect)); – JRafaelM May 12 '15 at 16:22
  • 1
    Excellent, but if the cells have a space between them then `visibleIndexPath` sometimes will be nil, So `if (visibleIndexPath) == nil { let cells = collectionView.visibleCells() let visibleIndexPath = collectionView.indexPathForCell(cells[0] as! UICollectionViewCell)! as NSIndexPath }` – Husam Jun 19 '15 at 17:19
  • The only solution that worked for my full screen size cells. Added Swift version as an answer below. – Sam Bing Apr 11 '16 at 12:43
  • Thanks a lot mate, I had my cell take up the whole screen. This worked the best. The other solution returned 2 indexPaths. Here's what i did in swift: let visibleRect:CGRect = CGRect(origin: self.collectionView!.contentOffset, size: self.collectionView!.bounds.size) let visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect)) let visibleIndexPath = self.collectionView?.indexPathForItemAtPoint(visiblePoint) – MD Singh Sep 08 '16 at 17:20
  • this is perfect! –  May 10 '17 at 07:02
  • Stackoverflow needs a function to allow people to vote or flag what the correct answer is. IndexPath for cell doesnt work if you try to get the visible index in cellForRow when a dequeue is happening and there is only one visible(full screen) cell at a time – some_id May 16 '17 at 14:31
  • how didEndDecelerating different from didEndAnimation. Both are called at the end.. – Amber K Sep 24 '18 at 16:04
  • This code is working as expected for me on collectionView cell 1st section, when scrolling down to the 2nd section, indexPath is coming "nil". – Mahendra Thotakura Apr 26 '19 at 07:21
  • 1
    I wish I could upvote this more. This problem has been driving me nuts and this solution saved the day. – SeanT Feb 26 '20 at 23:49
145

The method [collectionView visibleCells] give you all visibleCells array you want. Use it when you want to get

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    for (UICollectionViewCell *cell in [self.mainImageCollection visibleCells]) {
        NSIndexPath *indexPath = [self.mainImageCollection indexPathForCell:cell];
        NSLog(@"%@",indexPath);
    }
}

Update to Swift 5:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    for cell in yourCollectionView.visibleCells {
        let indexPath = yourCollectionView.indexPath(for: cell)
        print(indexPath)
    }
}
LE SANG
  • 10,607
  • 7
  • 55
  • 78
  • Can you please elaborate on where self.mainImageCollection comes from? Many thanx in advance for your further detail. – Benjamin McFerren Jun 01 '14 at 21:57
  • 1
    @BenjaminMcFerren it's the collectionview you used. – LE SANG Jun 02 '14 at 00:37
  • 11
    The `scrollViewDidScroll:` delegate method provides scroll view updates whenever any scroll finishes (and is probably a better choice here). As opposed to the `scrollViewDidEndDecelerating:` delegate method which is only called when the scroll view "*grinds to a halt [from a big scroll]*" (see UIScrollView header). – Sam Spencer Sep 16 '14 at 17:52
  • 3
    IIRC, `..DidScroll` gets called many times, even during a short scroll. May or may not be a good thing, depending on what one wants to do. – ToolmakerSteve Jun 28 '16 at 12:39
  • 4
    Since iOS 10 it's now also possible to use indexPathsForVisibleItems directly :) – Ben Nov 07 '16 at 14:03
  • Better to use self.collectionView?.indexPathsForVisibleItems for short. (added with ios 10 sdk) – JSBach May 16 '17 at 20:21
86

Swift 5:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    var visibleRect = CGRect()

    visibleRect.origin = collectionView.contentOffset
    visibleRect.size = collectionView.bounds.size

    let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)

    guard let indexPath = collectionView.indexPathForItem(at: visiblePoint) else { return } 

    print(indexPath)
}

Working Answers Combined In Swift 2.2 :

 func scrollViewDidEndDecelerating(scrollView: UIScrollView) {

        var visibleRect = CGRect()

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

        let visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect))

        let visibleIndexPath: NSIndexPath = self.collectionView.indexPathForItemAtPoint(visiblePoint)

        guard let indexPath = visibleIndexPath else { return } 
        print(indexPath)

    }
Sam Bing
  • 2,437
  • 17
  • 27
18

For completeness sake, this is the method that ended up working for me. It was a combination of @Anthony & @iAn's methods.

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
      CGRect visibleRect = (CGRect){.origin = self.collectionView.contentOffset, .size = self.collectionView.bounds.size};
      CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
      NSIndexPath *visibleIndexPath = [self.collectionView indexPathForItemAtPoint:visiblePoint];
      NSLog(@"%@",visibleIndexPath);
}
Meet Doshi
  • 3,983
  • 10
  • 35
  • 76
Vincil Bishop
  • 1,528
  • 15
  • 20
12

It will probably be best to use UICollectionViewDelegate methods: (Swift 3)

// Called before the cell is displayed    
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    print(indexPath.row)
}

// Called when the cell is displayed
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    print(indexPath.row)
}
Mr Stanev
  • 1,512
  • 1
  • 16
  • 22
  • 4
    Regarding `didEndDisplaying`, from the docs: **Tells the delegate that the specified cell was removed from the collection view. Use this method to detect when a cell is removed from a collection view, as opposed to monitoring the view itself to see when it disappears.** So I do not think `didEndDisplaying` is called when the cell is displayed. – Jonny Dec 26 '17 at 10:49
10

Just want to add for others : for some reason, I didnt not get the cell that was visible to the user when I was scrolling to previous cell in collectionView with pagingEnabled.

So I insert the code inside dispatch_async to give it some "air" and this works for me.

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    dispatch_async(dispatch_get_main_queue(), ^{
            UICollectionViewCell * visibleCell= [[self.collectionView visibleCells] objectAtIndex:0];


            [visibleCell doSomthing];
        });
}
user1105951
  • 2,117
  • 2
  • 28
  • 53
  • This really helped! I didn't realized I can use dispatch_async block to make it absolutely flawless! This is the best answer! – kalafun Oct 20 '15 at 15:45
  • 1
    For Swift 4: `DispatchQueue.main.async {` when call initiates from something like `func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {` – Jonny Dec 26 '17 at 10:33
  • Some clarification: this helped me too, and I had the same/similar problem, when scrolling back to uitableviewcell containing a uicollectionview. The refresh call initiated from `willDisplay cell` as mentioned above. I think the problem, somewhere in UIKit, is really the same in my case as this answer. – Jonny Dec 26 '17 at 10:39
8

UICollectionView current visible cell index: Swift 3, 4 and 5+

var visibleCurrentCellIndexPath: IndexPath? {
    for cell in self.collectionView.visibleCells {
        let indexPath = self.collectionView.indexPath(for: cell)
        return indexPath
     }
        
     return nil
}

As an Extension:

extension UICollectionView {
  var visibleCurrentCellIndexPath: IndexPath? {
    for cell in self.visibleCells {
      let indexPath = self.indexPath(for: cell)
      return indexPath
    }
    
    return nil
  }
}

Usage:

if let indexPath = collectionView.visibleCurrentCellIndexPath { 
   /// do something
}
Mihail Salari
  • 1,199
  • 11
  • 14
7

For Swift 3.0

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let visibleRect = CGRect(origin: colView.contentOffset, size: colView.bounds.size)
    let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
    let indexPath = colView.indexPathForItem(at: visiblePoint)
}
Varun Naharia
  • 4,836
  • 7
  • 42
  • 73
Hiren Panchal
  • 2,505
  • 22
  • 21
7

Swift 3.0

Simplest solution which will give you indexPath for visible cells..

yourCollectionView.indexPathsForVisibleItems 

will return the array of indexpath.

Just take the first object from array like this.

yourCollectionView.indexPathsForVisibleItems.first

I guess it should work fine with Objective - C as well.

Anil Kukadeja
  • 1,256
  • 1
  • 14
  • 26
3

converting @Anthony's answer to Swift 3.0 worked perfectly for me:

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    var visibleRect = CGRect()
    visibleRect.origin = yourCollectionView.contentOffset
    visibleRect.size = yourCollectionView.bounds.size
    let visiblePoint = CGPoint(x: CGFloat(visibleRect.midX), y: CGFloat(visibleRect.midY))
    let visibleIndexPath: IndexPath? = yourCollectionView.indexPathForItem(at: visiblePoint)
    print("Visible cell's index is : \(visibleIndexPath?.row)!")
}
2

You can use scrollViewDidEndDecelerating: for this

//@property (strong, nonatomic) IBOutlet UICollectionView *collectionView;

   - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{

        for (UICollectionViewCell *cell in [self.collectionView visibleCells]) {
            NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
            NSUInteger lastIndex = [indexPath indexAtPosition:[indexPath length] - 1];
            NSLog(@"visible cell value %d",lastIndex);
        }

    }
raaz
  • 11,942
  • 21
  • 59
  • 81
1

In this thread, There are so many solutions that work fine if cell takes full screen but they use collection view bounds and midpoints of Visible rect However there is a simple solution to this problem

    DispatchQueue.main.async {
        let visibleCell = self.collImages.visibleCells.first
        print(self.collImages.indexPath(for: visibleCell))
    }

by this, you can get indexPath of the visible cell. I have added DispatchQueue because when you swipe faster and if for a brief moment the next cell is shown then without dispactchQueue you'll get indexPath of briefly shown cell not the cell that is being displayed on the screen.

kuldip
  • 36
  • 6
0

This is old question but in my case...

- (void) scrollViewWillBeginDragging:(UIScrollView *)scrollView {

    _m_offsetIdx = [m_cv indexPathForCell:m_cv.visibleCells.firstObject].row;
}

- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    _m_offsetIdx = [m_cv indexPathForCell:m_cv.visibleCells.lastObject].row;
}
Hwangho Kim
  • 583
  • 3
  • 15
0

Also check this snippet

let isCellVisible = collectionView.visibleCells.map { collectionView.indexPath(for: $0) }.contains(inspectingIndexPath)
Nik Kov
  • 10,605
  • 4
  • 56
  • 96
-1

try this, it works. (in the example below i have 3 cells for example.)

    func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    let visibleRect = CGRect(origin: self.collectionView.contentOffset, size: self.collectionView.bounds.size)
    let visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect))
    let visibleIndexPath = self.collectionView.indexPathForItemAtPoint(visiblePoint)
    if let v = visibleIndexPath {
        switch v.item {
        case 0: setImageDescription()
            break
        case 1: setImageConditions()
            break
        case 2: setImageResults()
            break
        default: break
        }
    }
Arash Jamshidi
  • 103
  • 1
  • 6
-2

Swift 3 & Swift 4:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
   var visibleRect = CGRect()

   visibleRect.origin = collectionView.contentOffset
   visibleRect.size = collectionView.bounds.size

   let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)

   guard let indexPath = collectionView.indexPathForItem(at: visiblePoint) else { return } 

   print(indexPath[1])
}

If you want to show actual number than you can add +1

Jamil Hasnine Tamim
  • 3,459
  • 20
  • 39