10

Is it possible to add a snap-to position in a UITableView or UIScrollView? What I mean is not auto scroll to a position if I press a button or call some method to do it, I mean is if I scroll my scroll view or tableview around a specific point, say 0, 30, it will auto-snap to it and stay there? So if my scroll view or table view scrolls and then the user lets go inbetween 0, 25 or 0, 35, it will auto "snap" and scroll there? I can imagine maybe putting in an if-statement to test if the position falls in that area in either the scrollViewDidEndDragging:WillDecelerate: or scrollViewWillBeginDecelerating: methods of UIScrollView but I'm unsure how to implement this in the case of a UITableView. Any guidance would be much appreciated.

Milo
  • 5,041
  • 7
  • 33
  • 59
  • You're actually on the right line bro. Could you paste some code of what you have tried? – Pavan Jan 06 '14 at 00:15
  • I have seend your post Milo, and I also have replied to your post. will it be useful if I post an xcode project for you on how to do it properly so that you can have a dynamic scrollview where you dont need to constantly make changes to your scrollview delegate methods? It will be a much cleaner solution buddy :) let me know – Pavan Jan 06 '14 at 00:58
  • 1
    If you turn on `paging` for the scrollview, it will do this automatically. – John Gibb Sep 08 '14 at 19:56
  • Possible duplicate of [Want UITableView to "snap to cell"](https://stackoverflow.com/questions/16088615/want-uitableview-to-snap-to-cell) – gog Nov 12 '18 at 15:20

8 Answers8

26

Like Pavan stated, scrollViewWillEndDragging: withVelocity: targetContentOffset: is the method you should use. It works with table views and scroll views. The code below should work for you if you are using a table view or vertically scrolling scroll view. 44.0 is the height of the table cells in the sample code so you will need to adjust that value to the height of your cells. If used for a horizontally scrolling scroll view, swap the y's for x's and change the 44.0 to the width of the individual divisions in the scroll view.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    // Determine which table cell the scrolling will stop on.
    CGFloat cellHeight = 44.0f;
    NSInteger cellIndex = floor(targetContentOffset->y / cellHeight);

    // Round to the next cell if the scrolling will stop over halfway to the next cell.
    if ((targetContentOffset->y - (floor(targetContentOffset->y / cellHeight) * cellHeight)) > cellHeight) {
        cellIndex++;
    }

    // Adjust stopping point to exact beginning of cell.
    targetContentOffset->y = cellIndex * cellHeight;
}
kevinl
  • 4,194
  • 6
  • 37
  • 55
ninefifteen
  • 931
  • 1
  • 10
  • 16
  • Cheers bro, I did try telling him, a few times. But I think he's persistent in his own approach. – Pavan Jan 06 '14 at 17:14
6

I urge you to use the scrollViewWillEndDragging: withVelocity: targetContentOffset: method instead which is meant to be used for exactly your purpose. to set the target content offset to your desired position.

I also suggest you look at the duplicate questions already posted on SO.

Take a look at these posts

Pavan
  • 17,840
  • 8
  • 59
  • 100
4

If you really must do this manually, here is a Swift3 version. However, it's highly recommended to just turn on paging for the UITableView and this is handled for you already.

let cellHeight = 139
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    targetContentOffset.pointee.y = round(targetContentOffset.pointee.y / cellHeight) * cellHeight
}

// Or simply
self.tableView.isPagingEnabled = true
Mark McCorkle
  • 9,349
  • 2
  • 32
  • 42
3

In Swift 2.0 when the table has a content inset and simplifying things, ninefifteen's great answer becomes:

func scrollViewWillEndDragging(scrollView:            UIScrollView,
                               withVelocity velocity: CGPoint,
                               targetContentOffset:   UnsafeMutablePointer<CGPoint>)
{
    let cellHeight = 44
    let y          = targetContentOffset.memory.y + scrollView.contentInset.top + cellHeight / 2
    var cellIndex  = floor(y / cellHeight)

    targetContentOffset.memory.y = cellIndex * cellHeight - scrollView.contentInset.top;
}

By just adding cellHeight / 2, ninefifteen's if-statement to increment the index is no longer needed.

Community
  • 1
  • 1
meaning-matters
  • 21,929
  • 10
  • 82
  • 142
2

Here's the Swift 2.0 equivalent

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let cellWidth = CGRectGetWidth(frame) / 7   // 7 days
    var index = round(targetContentOffset.memory.x / cellWidth)
    targetContentOffset.memory.x = index * cellWidth
}

And this complicated rounding isn't necessary at all as long as you use round instead of floor

cldrr
  • 1,238
  • 14
  • 22
2

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
1

I believe that if you use delegate method:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    NSLog(@"%f",scrollView.contentOffset.y);
    if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370)
    {
        NSLog(@"setContentOffset");
        [scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
    }
}

Play around with it until you get the desired behaviour. Maybe calculate the next top edge of the next tableViewCell/UIView and stop at the top of the nearest one at the slow down.

The reason for this: if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370) is that the scroll view calls scrollViewDidScroll in jumps at fast speeds so I give a between if.

You can also know the speed is slowing down by:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"%f",scrollView.contentOffset.y);
    int scrollSpeed = abs(scrollView.contentOffset.y - previousScrollViewYOffset);
    previousTableViewYOffset = scrollView.contentOffset.y;
    if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370 && scrollSpeed < minSpeedToStop)
    {
        NSLog(@"setContentOffset");
        [scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
    }
}
Refael.S
  • 1,634
  • 1
  • 12
  • 22
0

Updated for Swift 5

As a bonus, it works for whatever the height is of the cell it was going to land on

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    guard let indexPath = self.tableView.indexPathForRow(at: CGPoint(x: self.tableView.frame.midX, y: targetContentOffset.pointee.y)) else {
        return
    }
    var cellHeight = tableView(self.tableView, heightForRowAt: indexPath)
    let cellIndex = floor(targetContentOffset.pointee.y / cellHeight)
    
    if targetContentOffset.pointee.y - (floor(targetContentOffset.pointee.y / cellHeight) * cellHeight) > cellHeight {
        cellHeight += 1
    }
    targetContentOffset.pointee.y = cellIndex * cellHeight
}
    
Will
  • 4,942
  • 2
  • 22
  • 47