Best way to check if UITableViewCell is completely visible

IosUitableviewVisible

Ios Problem Overview


I have a UITableView with cells of different heights and I need to know when they are completely visible or not.

At the moment I am looping through each cell in the list of visible cells to check if it is completely visible every time the view is scrolled . Is this the best approach?

Here's my code:

- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
    
    CGPoint offset = aScrollView.contentOffset;
    CGRect bounds = aScrollView.bounds;    
    NSArray* cells = myTableView.visibleCells;
    
    for (MyCustomUITableViewCell* cell in cells) {
        
        if (cell.frame.origin.y > offset.y &&
            cell.frame.origin.y + cell.frame.size.height < offset.y + bounds.size.height) {
            
            [cell notifyCompletelyVisible];
        }
        else {
           
            [cell notifyNotCompletelyVisible];
        }
    }
}

Edit:

Please note that *- (NSArray )visibleCells returns visible cells which are both completely visible and partly visible.

Edit 2:

This is the revised code after combining solutions from both lnafziger and Vadim Yelagin:

- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
    NSArray* cells = myTableView.visibleCells;
    NSArray* indexPaths = myTableView.indexPathsForVisibleRows;
    
    NSUInteger cellCount = [cells count];

    if (cellCount == 0) return;
    
    // Check the visibility of the first cell
    [self checkVisibilityOfCell:[cells objectAtIndex:0] forIndexPath:[indexPaths objectAtIndex:0]];

    if (cellCount == 1) return;
    
    // Check the visibility of the last cell
    [self checkVisibilityOfCell:[cells lastObject] forIndexPath:[indexPaths lastObject]];
    
    if (cellCount == 2) return;
    
    // All of the rest of the cells are visible: Loop through the 2nd through n-1 cells
    for (NSUInteger i = 1; i < cellCount - 1; i++)
        [[cells objectAtIndex:i] notifyCellVisibleWithIsCompletelyVisible:YES];
}

- (void)checkVisibilityOfCell:(MultiQuestionTableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath {
    CGRect cellRect = [myTableView rectForRowAtIndexPath:indexPath];
    cellRect = [myTableView convertRect:cellRect toView:myTableView.superview];
    BOOL completelyVisible = CGRectContainsRect(myTableView.frame, cellRect);
    
    [cell notifyCellVisibleWithIsCompletelyVisible:completelyVisible];
}

Ios Solutions


Solution 1 - Ios

You can get the rect of a cell with rectForRowAtIndexPath: method and compare it with tableview's bounds rect using CGRectContainsRect function.

Note that this will not instantiate the cell if it is not visible, and thus will be rather fast.

Swift

let cellRect = tableView.rectForRowAtIndexPath(indexPath)
let completelyVisible = tableView.bounds.contains(cellRect)

Obj-C

CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
BOOL completelyVisible = CGRectContainsRect(tableView.bounds, cellRect);

Of course this will not regard the table view being clipped by a superview or obscured by another view.

Solution 2 - Ios

I would change it like this:

- (void)checkVisibilityOfCell:(MyCustomUITableViewCell *)cell inScrollView:(UIScrollView *)aScrollView {
    CGRect cellRect = [aScrollView convertRect:cell.frame toView:aScrollView.superview];

    if (CGRectContainsRect(aScrollView.frame, cellRect))
        [cell notifyCompletelyVisible];
    else
        [cell notifyNotCompletelyVisible];
}

- (void)scrollViewDidScroll:(UIScrollView *)aScrollView { 
    NSArray* cells = myTableView.visibleCells;
    
    NSUInteger cellCount = [cells count];
    if (cellCount == 0)
        return;

    // Check the visibility of the first cell
    [self checkVisibilityOfCell:[cells firstObject] inScrollView:aScrollView];
    if (cellCount == 1)
        return;

    // Check the visibility of the last cell
    [self checkVisibilityOfCell:[cells lastObject] inScrollView:aScrollView];
    if (cellCount == 2)
        return;

    // All of the rest of the cells are visible: Loop through the 2nd through n-1 cells
    for (NSUInteger i = 1; i < cellCount - 1; i++)
        [[cells objectAtIndex:i] notifyCompletelyVisible];
}

Solution 3 - Ios

You can try something like this to see how much percentage is visible:

-(void)scrollViewDidScroll:(UIScrollView *)sender
{
    [self checkWhichVideoToEnable];
}

-(void)checkWhichVideoToEnable
{
    for(UITableViewCell *cell in [tblMessages visibleCells])
    {
        if([cell isKindOfClass:[VideoMessageCell class]])
        {
            NSIndexPath *indexPath = [tblMessages indexPathForCell:cell];
            CGRect cellRect = [tblMessages rectForRowAtIndexPath:indexPath];
            UIView *superview = tblMessages.superview;
            
            CGRect convertedRect=[tblMessages convertRect:cellRect toView:superview];
            CGRect intersect = CGRectIntersection(tblMessages.frame, convertedRect);
            float visibleHeight = CGRectGetHeight(intersect);
            
            if(visibleHeight>VIDEO_CELL_SIZE*0.6) // only if 60% of the cell is visible
            {
                // unmute the video if we can see at least half of the cell
                [((VideoMessageCell*)cell) muteVideo:!btnMuteVideos.selected];
            }
            else
            {
                // mute the other video cells that are not visible
                [((VideoMessageCell*)cell) muteVideo:YES];
            }
        }
    }
}

Solution 4 - Ios

If you also want to take the contentInset into account, and don't want to rely on a superview (the table view frame in superview could be something else than 0,0), here's my solution:

extension UITableView {

    public var boundsWithoutInset: CGRect {
        var boundsWithoutInset = bounds
        boundsWithoutInset.origin.y += contentInset.top
        boundsWithoutInset.size.height -= contentInset.top + contentInset.bottom
        return boundsWithoutInset
    }

    public func isRowCompletelyVisible(at indexPath: IndexPath) -> Bool {
        let rect = rectForRow(at: indexPath)
        return boundsWithoutInset.contains(rect)
    }
}

Solution 5 - Ios

From the docs:

> visibleCells Returns the table cells that are visible in the receiver. > > - (NSArray *)visibleCells > > Return Value An array containing UITableViewCell objects, each > representing a visible cell in the > receiving table view. > > Availability Available in iOS 2.0 and later. > > See Also – > indexPathsForVisibleRows

Solution 6 - Ios

Swift 5+

we can use

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
       ...
    }

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    ...
}


let cellRect = tableView.rectForRow(at: indexPath)
let completelyVisible = tableView.bounds.contains(cellRect)

Solution 7 - Ios

The code below will let you check if a collection view cell is completely visible through the layout attributes of the collection view.

guard let cellRect = collectionView.layoutAttributesForItem(at: indexPath)?.frame else { return } let isCellCompletelyVisible = collectionView.bounds.contains(cellRect)

Solution 8 - Ios

UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
CGRect frame = cell.frame;
if (CGRectContainsRect(CGRectOffset(self.collectionView.frame, self.collectionView.contentOffset.x, self.collectionView.contentOffset.y), frame))
{
    // is on screen
}

Solution 9 - Ios

- (BOOL)checkVisibilityOfCell{
    if (tableView.contentSize.height <= tableView.frame.size.height) {
        return YES;
    } else{
        return NO;
    }
}

Solution 10 - Ios

Even if you said you want to check it every time you scrolled, you can also use

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
       CGRect cellRect = [tableView convertRect:cell.frame toView:tableView.superview];
       if (CGRectContainsRect(tableView.frame, cellRect)){
            //Do things in case cell is fully displayed
        }

}

Solution 11 - Ios

Maybe for this issue better used next function from UITableViewDelegate

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRohinNZView Question on Stackoverflow
Solution 1 - IosVadim YelaginView Answer on Stackoverflow
Solution 2 - IoslnafzigerView Answer on Stackoverflow
Solution 3 - IosCatalinView Answer on Stackoverflow
Solution 4 - IosKukoskView Answer on Stackoverflow
Solution 5 - IosCodaFiView Answer on Stackoverflow
Solution 6 - IosBinoy joseView Answer on Stackoverflow
Solution 7 - IosfahloutView Answer on Stackoverflow
Solution 8 - IosAndy PoesView Answer on Stackoverflow
Solution 9 - IosNaresh JainView Answer on Stackoverflow
Solution 10 - IosiRestMyCaseYourHonorView Answer on Stackoverflow
Solution 11 - IosArtyomView Answer on Stackoverflow