Want UITableView to "snap to cell" - ios

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?

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];
}

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.

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];
}

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;
}

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;
}

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

UITableView extends UIScrollView...
myTableView.pagingEnabled = YES

Related

Disable scrollViewDidScroll: when scrolling UICollectionView - iOS

I have implemented scrollViewDidScroll: inside my viewcontroller to cause some animations when I scroll the view up and down.
However, when I scroll my collectionview inside the viewcontroller (horizontally) it messes up with my animation inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
newAlpha = 1 - scrollView.contentOffset.y / 200;
self.introImageView.alpha = newAlpha;
//... -> prevent scrolling when collectionview is scrolled
}
How do I prevent calling scrollViewDidScroll: when scrolling my collectionview horizontally?
The best way is not to disable the delegate method, but make sure to only call that code when it's called by your scrollview. Here's an example
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.myScrollView) {
newAlpha = 1 - scrollView.contentOffset.y / 200;
self.introImageView.alpha = newAlpha;
} else {
//collectionView would fall here
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ([scrollView isKindOfClass:[UICollectionView class]] == NO) {
newAlpha = 1 - scrollView.contentOffset.y / 200;
self.introImageView.alpha = newAlpha;
//... -> prevent scrolling when collectionview is scrolled
}
}

UITableView with one visible cell: determine which is most visible

Given a UITableView with a single visible cell at any given time, how can I determine which cell is most in view while the table view is being scrolled?
I know I can get an array of visible cells by doing this:
NSArray *paths = [tableView indexPathsForVisibleRows];
And then get the last cell (or first, or whatever) by doing:
UITableViewCell* cell = (UITableViewCell*)[tableView cellForRowAtIndexPath:[paths lastObject]];
But how to I compare all the visible cells and determine which of them is most in view?
The following logic would get you the most visible cell at the end of the scroll:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGRect visibleRect = (CGRect){.origin = self.tableView.contentOffset, .size = self.tableView.bounds.size};
CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
NSIndexPath *visibleIndexPath = [self.tableView indexPathForRowAtPoint:visiblePoint];
}
The algorithm is different depending on how many paths you get back:
If there is only one path, that's the most visible cell right there
If there are three or more paths, any of the cells in the middle (i.e. all cells except the first and the last ones) are equally visible
If there are exactly two cells, find the position of the line that separates the two in their parent view*, and compute two distances - top-to-middle and middle-to-bottom. If top-to-middle is greater, then the top cell is most visible. If middle-to-bottom is greater, then the second cell is more visible. Otherwise, the two cells are equally visible.
* Midpoint position is the bottom of the second cell. Top and bottom positions are the top and bottom of the table view.
Swift solution based on #Sebyddd's answer:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
scrollToMostVisibleCell()
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate{
scrollToMostVisibleCell()
}
}
func scrollToMostVisibleCell(){
let visibleRect = CGRect(origin: tableView.contentOffset, size: tableView.bounds.size)
let visiblePoint = CGPoint(x: CGRectGetMidX(visibleRect), y: CGRectGetMidY(visibleRect))
let visibleIndexPath: NSIndexPath = tableView.indexPathForRowAtPoint(visiblePoint)!
tableView.scrollToRowAtIndexPath(visibleIndexPath, atScrollPosition: .Top, animated: true)
}
You can use the table view's rectForRowAtIndexPath: to get the frame of each visible cell, then offset them (with CGRectOffset) by -contentOffset.y to account for scrolling, then intersect them with the table view's bounds to find out how much each cell is visible inside the table view.
The below logic will give you the UITableViewCell which is most visible or closet to center in UITableView every time as soon as user stops scrolling. Hope this logic would help somebody.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
if (isScrollingStart)
{
isScrollingStart=NO;
isScrollingEnd=YES;
[self scrollingStopped];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (isScrollingStart)
{
isScrollingStart=NO;
isScrollingEnd=YES;
[self scrollingStopped];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
isScrollingStart=YES;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
isScrollingStart=YES;
}
-(void)scrollingStopped
{
NSMutableArray* arrVideoCells=[NSMutableArray array];
NSLog(#"Scrolling stopped");
NSArray* arrVisibleCells=[self.tableTimeline visibleCells];
for (TimeLineCell* cell in arrVisibleCells)
{
if ([cell isKindOfClass:[TimeLineCellMediaVideo class]])
{
[arrVideoCells addObject:cell];
}
}
TimeLineCellMediaVideo* videoCell=[self getCellNearCenterOfScreen:arrVideoCells];
}
-(TimeLineCellMediaVideo*)getCellNearCenterOfScreen:(NSMutableArray*)arrCells
{
TimeLineCellMediaVideo* closetCellToCenter;
CGRect filterCGRect;
for (TimeLineCellMediaVideo* videoCell in arrCells)
{
if (arrCells.count==1)
closetCellToCenter= videoCell;
NSIndexPath* cellIndexPath=[self.tableTimeline indexPathForCell:videoCell];
CGRect rect = [self.tableTimeline convertRect:[self.tableTimeline rectForRowAtIndexPath:cellIndexPath] toView:[self.tableTimeline superview]];
if (closetCellToCenter)
{
CGRect intersect = CGRectIntersection(self.tableTimeline.frame, filterCGRect);
float visibleHeightFilterCell = CGRectGetHeight(intersect);
intersect = CGRectIntersection(self.tableTimeline.frame, rect);
float visibleHeightCurrentCell = CGRectGetHeight(intersect);
if (visibleHeightCurrentCell>visibleHeightFilterCell)
{
filterCGRect=rect;
closetCellToCenter= videoCell;
}
}
else
{
closetCellToCenter=videoCell;
filterCGRect=rect;
}
}
return closetCellToCenter;
}
I did the following to find indexPath for most visible cell and it is working correctly.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard let tableView = scrollView as? UITableView else {
return
}
let visibleHeights = tableView.visibleCells.compactMap { cell -> (indexPath: IndexPath, visibleHeight: CGFloat)? in
guard let indexPath = tableView.indexPath(for: cell) else {
return nil
}
let cellRect = tableView.rectForRow(at: indexPath)
let superView = tableView.superview
let convertedRect = tableView.convert(cellRect, to: superView)
let intersection = tableView.frame.intersection(convertedRect)
let visibleHeight = intersection.height
return (indexPath, visibleHeight)
}
guard let maxVisibleIndexPath = visibleHeights.max(by: { $0.visibleHeight < $1.visibleHeight })?.indexPath else {
return
}
print("maxVisibleIndexPath: \(maxVisibleIndexPath)")
}

How can I detect the scroll direction from the UICollectionView?

I have a UICollectionView. I want to detect scroll direction. I have a two different animation style for scroll down and scroll up. So I must learn scroll direction.
CGPoint scrollVelocity = [self.collectionView.panGestureRecognizer
velocityInView:self.collectionView.superview];
if (scrollVelocity.y > 0.0f)
NSLog(#"scroll up");
else if(scrollVelocity.y < 0.0f)
NSLog(#"scroll down");
This is just work at finger touched. Not work for me
Try this:
Add this somewhere in you header:
#property (nonatomic) CGFloat lastContentOffset;
Then override the scrollViewDidScroll: method:
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.lastContentOffset > scrollView.contentOffset.y)
{
NSLog(#"Scrolling Up");
}
else if (self.lastContentOffset < scrollView.contentOffset.y)
{
NSLog(#"Scrolling Down");
}
self.lastContentOffset = scrollView.contentOffset.y;
}
Found in Finding the direction of scrolling in a UIScrollView?
this is the best way to get scroll direction, hope this helps you
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint targetPoint = *targetContentOffset;
CGPoint currentPoint = scrollView.contentOffset;
if (targetPoint.y > currentPoint.y) {
NSLog(#"up");
}
else {
NSLog(#"down");
}
}
Swift 4.2
private var lastContentOffset: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if lastContentOffset > scrollView.contentOffset.y && lastContentOffset < scrollView.contentSize.height - scrollView.frame.height {
// move up
print("move up")
originalHeight ()
} else if lastContentOffset < scrollView.contentOffset.y && scrollView.contentOffset.y > 0 {
// move down
print("move down")
minimizeHeaderView()
}
// update the new position acquired
lastContentOffset = scrollView.contentOffset.y
}
I was trying to find a way to detect if the user is mostly trying to pull vertically or horizontally the scrollView. I give you my solution, I hope it can be useful to anyone :
CGPoint _lastContentOffset;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
_lastContentOffset = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (ABS(_lastContentOffset.x - scrollView.contentOffset.x) < ABS(_lastContentOffset.y - scrollView.contentOffset.y)) {
NSLog(#"Scrolled Vertically");
} else {
NSLog(#"Scrolled Horizontally");
}
}
This work find for me and I use this to avoid the scrollView to move horizontally when scrolling vertically and opposite.

UICollectionView: how to detect when scrolling has stopped

I'm using a UICollectionView to scroll through a set of thumbnails quickly. Once scrolling ends, I'd like to display a larger hi-res version of the current thumbnail.
How can I detect when the user has completed scrolling? I do implement didEndDisplayingCell, but that only tells me when a particular cell has scrolled off; it doesn't tell me when the scroll motion actually completes.
NS_CLASS_AVAILABLE_IOS(6_0) #interface UICollectionView : UIScrollView
UICollectionView is a subclass of UIScrollView. So if you have set the delegate and implemented UIScrollViewDelegate, you should be able to detect this the same way as UIScrollView.
For eg:-
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
As per documentation, the above method should tell when the scroll view has ended decelerating the scrolling movement.
Just to cover your bases you should implement both these UIScrollViewDelegate methods.
In some cases there may not be a deceleration (and scrollViewDidEndDecelerating would not be called), for e.g., the page is fully scrolled in place. In those case do your update right there in scrollViewDidEndDragging.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self updateStuff];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self updateStuff];
}
An important fact to note here.
This method gets called on User initiated scrolls (i.e a Pan gesture):
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
or in Swift:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
On the other hand, this one gets called on all manually (programatically) initiated scrolls (like scrollRectToVisible or scrollToItemAtIndexPath):
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
or in Swift:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
Swift 3 version of Abey M and D6mi 's answers:
When scroll is caused by user action
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if (!decelerate) {
//cause by user
print("SCROLL scrollViewDidEndDragging")
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
//caused by user
print("SCROLL scrollViewDidEndDecelerating")
}
When scroll is caused by code action (programmatically): (like "scrollRectToVisible" or "scrollToItemAtIndexPath")
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
//caused by code
print("SCROLL scrollViewDidEndScrollingAnimation")
}
Notes:
Put these functions in your UIScrollViewDelegate or UICollectionViewDelegate delegate.
if you don't have a separate delegate, make your current class extend a UIScrollViewDelegate op top of your class file
.
open class MyClass: NSObject , UICollectionViewDelegate
and somewhere in your viewWillAppear make the class its own delegate
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// ...
self.myScrollView.delegate = self
// ...
}
Swift 3 version:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Your code here
}
if you want to use the visible indexpath:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self scrollingFinish];
}
- (void)scrollingFinish {
if([self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionHeader]){
NSIndexPath *firstVisibleIndexPath = [[self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionHeader] firstObject];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIndexPath atScrollPosition:UICollectionViewScrollPositionTop animated:YES];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
}
Get your collection view index and Dont forget to import UISCrollViewDelegate in your class
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let xPoint = scrollView.contentOffset.x + scrollView.frame.width / 2
let yPoint = scrollView.frame.height / 2
let center = CGPoint(x: xPoint, y: yPoint)
if let ip = self.collectionView.indexPathForItem(at: center) {
pageIndex = ip.row
}
print(">>>>>>>>>\(pageIndex)")
}

Detect UITextView scroll location

I am trying to implement a form of a Terms & Conditions page where the "Proceed" button is only enabled once the user has scrolled to the bottom of a UITextView. So far I have set my class as a UIScrollView delegate & have implemented the method below:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSLog(#"Checking if at bottom of UITextView");
CGPoint bottomOffset = CGPointMake(0,self.warningTextView.frame.size.height);
//if ([[self.warningTextView contentOffset] isEqualTO:bottomOffset])
{
}
}
I have commented the if statement because I am not sure how to check if the UITextView is at the bottom.
UITextView is a UIScrollView subclass. Therefore the UIScrollView delegate method you are using is also available when using UITextView.
Instead of using scrollViewDidEndDecelerating, you should use scrollViewDidScroll, as the scrollview may stop scrolling without deceleration.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height)
{
NSLog(#"at bottom");
}
}
A Swift version for this question:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height {
print( "View scrolled to the bottom" )
}
}
This should solve it. It works. I am using it.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height;
if (bottomEdge >= scrollView.contentSize.height)
{
// we are at the end
}
}

Resources