Want UITableView to "snap to cell"
Asked Answered
O

7

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?

Oratorical answered 18/4, 2013 at 16:40 Comment(2)
you can do this by changing ContentOffset of table.Fichtean
you can use, scrollview delegate method and contentoffset feature of UITableview, as Nishant suggest :)Expeller
D
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];
}
Drida answered 18/4, 2013 at 16:51 Comment(4)
Thank you! The piece I was looking for was how to detect which cell was the centermost (your centerTable method).Oratorical
This will work, but it will cause a weird little scroll after the table view comes to rest.Cuevas
Agree with Jesse Rusak. Looking for a snap to during the scroll.Vetavetch
Posting an answer with this.Vetavetch
C
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.

Cuevas answered 18/4, 2013 at 16:59 Comment(5)
Thank you. This is useful as well.Oratorical
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?Harass
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.Harass
@Harass 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.Cuevas
Need brackets to get the correct value: targetContentOffset->y = (100 * (int)targetContentOffset->y/100);Bezel
V
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];
}
Vetavetch answered 21/10, 2013 at 23:21 Comment(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: https://mcmap.net/q/234981/-how-can-i-programmatically-force-stop-scrolling-in-a-uiscrollview, then it snaps as soon as you STOP dragging, but still no mid-drag snapping.Monteverdi
U
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;
}
Unreasonable answered 26/3, 2014 at 21:24 Comment(0)
N
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;
}
Nylanylghau answered 12/11, 2018 at 15:7 Comment(0)
A
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
Agathy answered 25/2, 2017 at 10:56 Comment(0)
E
-3

UITableView extends UIScrollView...

myTableView.pagingEnabled = YES 
Earwax answered 12/8, 2013 at 21:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.