21

I am displaying fairly large images in a UITableView. As the user scrolls, I'd like to the table view to always snap the center-most photo in the middle. That is, when the table is in a resting state, it will always have a UITableViewCell snapped to the center.

How does one do this?

jszumski
  • 7,430
  • 11
  • 40
  • 53
VaporwareWolf
  • 10,143
  • 10
  • 54
  • 80

7 Answers7

29

You can use the UIScrollViewDelegate methods on UITableView to do this:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    // if decelerating, let scrollViewDidEndDecelerating: handle it
    if (decelerate == NO) {
        [self centerTable];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self centerTable];
}

- (void)centerTable {
    NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))];

    [self.tableView scrollToRowAtIndexPath:pathForCenterCell atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
jszumski
  • 7,430
  • 11
  • 40
  • 53
18

There is a UIScrollView delegate method especially for this!

Edit: if you just want the code, look at the answers below which build off this.

The table view (which is a scroll view) will call - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset when the user stops scrolling. You can set manipulate the targetContentOffset to ensure it ends up where you want, and it will decelerate ending at that position (just like a paging UIScrollView).

For example, if your cells were all 100 points high, you could do:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    targetContentOffset->y = 100 * (int)targetContentOffset->y/100;
}

Of course, you can also inspect the targetContentOffset passed in to see where it was going to land, and then find the cell that is in and alter it appropriately.

Jesse Rusak
  • 56,530
  • 12
  • 101
  • 102
  • Thank you. This is useful as well. – VaporwareWolf Apr 18 '13 at 17:35
  • Your code throws a warning in Xcode 6.3 with iOS 8. I had to use the following code, but it's not doing anything. targetContentOffset->x = 100 * (int)targetContentOffset/100; Any idea why? – user3344977 Apr 11 '15 at 05:23
  • Hey Jesse I noticed your comment on the answer above "This will work, but it will cause a weird little scroll after the table view comes to rest." - If I can get your answer to work, will it produce more of a paging / snap to scroll effect? Really appreciate the help. – user3344977 Apr 11 '15 at 05:24
  • @user3344977 Yes, this answer is a paging/snap rather than causing another scroll after it stops. You want `->y` not `->x`; I've updated the answer. You might also look at @mikepj's answer below. – Jesse Rusak Apr 11 '15 at 13:57
  • Need brackets to get the correct value: targetContentOffset->y = (100 * (int)targetContentOffset->y/100); – stevex Nov 22 '16 at 11:54
6

Building from what @jszumski posted, if you want the snap to occur mid drag, use this code:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    [self centerTable];
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
    [self centerTable];
}

- (void)centerTable {
    NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))];

    [self.tableView scrollToRowAtIndexPath:pathForCenterCell atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
quantumpotato
  • 9,637
  • 14
  • 70
  • 146
  • 1
    I'm not sure what this answer means by "mid drag", but having implemented it, I've verified that it waits until the drag momentum settles before snapping in to place. This makes sense, since didEndDragging and willBeginDecelerating aren't called until then. It's much closer if you cancel ongoing scroll using this method: http://stackoverflow.com/a/3421387/111243, then it snaps as soon as you STOP dragging, but still no mid-drag snapping. – SimplGy Jun 17 '15 at 10:12
6

Extending @jesse-rusak's answer above, this is the code you would need to add to your UITableViewController subclass if you have cells with variable heights. This will avoid the double-scroll issue in the accepted answer.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    NSIndexPath *pathForTargetTopCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), targetContentOffset->y)];
    targetContentOffset->y = [self.tableView rectForRowAtIndexPath:pathForTargetTopCell].origin.y;
}
mikepj
  • 1,256
  • 11
  • 11
4

Extending @mikepj answer, (which in turn extended the great answer by @JesseRusak), this code lets you snap to a cell, even when cells have a variable (or unknown) height, and will snap to the next row if you'll scroll over the bottom half of the row, making it more "natural".

Original Swift 4.2 code: (for convenience, this is the actual code I developed and tested)

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    guard var scrollingToIP = table.indexPathForRow(at: CGPoint(x: 0, y: targetContentOffset.pointee.y)) else {
        return
    }
    var scrollingToRect = table.rectForRow(at: scrollingToIP)
    let roundingRow = Int(((targetContentOffset.pointee.y - scrollingToRect.origin.y) / scrollingToRect.size.height).rounded())
    scrollingToIP.row += roundingRow
    scrollingToRect = table.rectForRow(at: scrollingToIP)
    targetContentOffset.pointee.y = scrollingToRect.origin.y
}

(translated) Objective-C code: (since this question is tagged objective-c)

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    NSIndexPath *scrollingToIP = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    if (scrollingToIP == nil)
        return;
    CGRect scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
    NSInteger roundingRow = (NSInteger)(round(targetContentOffset->y - scrollingToRect.origin.y) / scrollingToRect.size.height));
    scrollingToIP.row += roundingRow;
    scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
    targetContentOffset->y = scrollingToRect.origin.y;
}
gog
  • 1,220
  • 11
  • 30
0

for the swift peeps

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) 
{
   if decelerate == false
   {
       self.centerTable()
   }
 }

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    self.centerTable()
}

func centerTable()
{
   let midX:CGFloat = self.tableView.bounds.midX
   let midY:CGFloat = self.tableView.bounds.midY
   let midPoint:CGPoint = CGPoint(x: midX, y: midY)

   if let pathForCenterCell:IndexPath = self.tableView .indexPathForRow(at: midPoint)
    {
      self.tableView.scrollToRow(at: pathForCenterCell, at: .middle, animated: true)
    }
}//eom
LuAndre
  • 1,114
  • 12
  • 23
-3

UITableView extends UIScrollView...

myTableView.pagingEnabled = YES 
jh314
  • 27,144
  • 16
  • 62
  • 82