Auto-snap to section of UITableView

While working on the Toast project, a social networking app that emphasizes user content, I had the thought that it would be a nice feature to display content in a way where it tries to fit a post into the iPhone’s frame.

Because it’s social networking, we have the original post and then user comments.  In iOS speak, this made sense to use a UITableView’s section for each of these objects, then the number of rows in that section would be directly related to the number of comments surrounding that post.

So, what am I getting at?  I came up with some code that basically says “If a section is near the top of the screen, then automatically snap him into place, so that he is at the top of the screen.”

Programmatically speaking, it would be “once the table view stops moving (decelerating) check if there’s a section start (row = 0) whose origin is near the top of the frame, and if he’s within a certain threshold distance, then automatically scroll the table view to place him at the top of the view’s frame.

And it’s dead simple.  Just use the UIScrollViewDelegate methods (which a UITableView also sends)

// after touch up and it stopped scrolling.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self autosnapSectionIfPossible];
}

// you could put this at the top of your .h if you wanted to...
#define kAutoSnapThreshold 75.0f

- (void)autosnapSectionIfPossible
{
    // if you're at the bottom of the table, don't let auto-snap scroll you back up.
    if (self.table.contentOffset.y >= self.table.contentSize.height - self.table.frame.size.height - 1) {
        //don't autosnap
        return;
    }

    // get the indexPaths that are currently in view.
    NSArray *visiblePaths = nil;
    visiblePaths = [self.table indexPathsForVisibleRows];

    // there can be one of two sections that might be closest to the top of the view
    NSIndexPath *firstPath = nil, *secondPath = nil;

    for (NSIndexPath *p in visiblePaths) {
        // row 0 corresponds to the top of your data object / start of a section
        if (p.row == 0 && firstPath == nil) {
            firstPath = p;
            continue;
        }
        else if (p.row == 0 && firstPath != nil)
        {
            secondPath = p;
            break;
        }

    }

    if (firstPath == nil && secondPath == nil) {
        // then there was none, so don't do the rest
        return;
    }

    UITableViewCell *firstCell = [self.table cellForRowAtIndexPath:firstPath];  // will be nil if path is nil
    UITableViewCell *secondCell = [self.table cellForRowAtIndexPath:secondPath];  // will be nil if path is nil

    UITableViewCell *theCell = nil;  // the actual cell of interest.  Will be either firstCell or secondCell
    NSIndexPath     *thePath = nil;  // likewise

    CGPoint firstOrigin;
    CGPoint secondOrigin;
    CGPoint theOrigin;

    // if it might be one of two cells
    if (firstCell && secondCell) {
        firstOrigin = [firstCell.superview convertPoint:firstCell.frame.origin toView:[self.table superview]];
        secondOrigin = [secondCell.superview convertPoint:secondCell.frame.origin toView:[self.table superview]];
        
        // if the first one is closer to the top of the tableview than the second one.
        if (fabsf(firstOrigin.y) < fabsf(secondOrigin.y)){
            theOrigin = firstOrigin;
            theCell = firstCell;
            thePath = firstPath;
        }
        else{
            theOrigin = secondOrigin;
            theCell = secondCell;
            thePath = secondPath;
        }
    }
    else if (firstCell)
    {
        theOrigin = [firstCell.superview convertPoint:firstCell.frame.origin toView:[self.table superview]];
        theCell = firstCell;
        thePath = firstPath;
    }
    else if (secondCell)
    {
        theOrigin = [secondCell.superview convertPoint:secondCell.frame.origin toView:[self.table superview]];
        theCell = secondCell;
        thePath = secondPath;
    }
    else{
        // else we don't want any auto-snapping, so we make the cell's origin as far away from 0 as possible.  See next few lines as to why it's done like this
        theOrigin = CGPointMake(0, CGFLOAT_MAX);  
    }
    
    // now we know which one is closer, but is he close enough?  
    if (fabsf(theOrigin.y) > kAutosnapThreshold) {
        theCell = nil; // there is no snapping to take place, so we aren't interested in auto-snapping to any cell
    }
    
    if (theCell) {
        [self.table scrollToRowAtIndexPath:thePath atScrollPosition:UITableViewScrollPositionTop animated:YES];
    }
}

Just copy-paste and try! Note, this doesn’t make too much sense for individual cells (unless they are pretty tall cells), and you’d have to modify the code if you want to use just cells and not sections.

Advertisements

2 thoughts on “Auto-snap to section of UITableView

  1. Great idea and code, but you forgot to set tha values for theCell and thePath so scrollToIndexPath is never called. 😉 The missing lines should look like this:
    if (fabsf(firstOrigin.y)<fabs(secondOrigin.y) && fabsf(firstOrigin.y) fabs(secondOrigin.y) && fabsf(secondOrigin.y) < kAutoSnapThreshold) {
    theCell = secondCell; //snap to second cell if closer
    }

    if (theCell) {
    thePath = [self.tableView indexPathForCell:theCell]; // get the indexpath for the cell
    [self.table scrollToRowAtIndexPath:thePath atScrollPosition:UITableViewScrollPositionTop animated:YES];
    }

    • Oh man! Looks like you found my own copy-paste bug. Went into my code where I used this and it didn’t match the post. Although your code also wasn’t quite right, thanks for pointing out that I had to take another look at this! Updated the post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s