Add snap-to position in a UITableView or UIScrollView - ios

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.

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

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

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

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.

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

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

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

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
}

Related

UICollectionView with pagination. Keeping the second cell in center of the screen

I am new to iOS.
I am having my collection view inside tableview cell.
There are 3 cells in collection view cell.
I need to show the second cell of collection view in center of the screen as shown in the image and also want to add pagination into it.
Any help will be appreciated.
Image
Thank You
I had to do a similar collection view a few months ago, This is the code that I use:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let layout = theNameOfYourCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidthIncludingSpacing = layout.itemSize.width + layout.minimumLineSpacing // Calculate cell size
let offset = scrollView.contentOffset.x
let index = (offset + scrollView.contentInset.left) / cellWidthIncludingSpacing // Calculate the cell need to be center
if velocity.x > 0 { // Scroll to -->
targetContentOffset.pointee = CGPoint(x: ceil(index) * cellWidthIncludingSpacing - scrollView.contentInset.right, y: -scrollView.contentInset.top)
} else if velocity.x < 0 { // Scroll to <---
targetContentOffset.pointee = CGPoint(x: floor(index) * cellWidthIncludingSpacing - scrollView.contentInset.left, y: -scrollView.contentInset.top)
} else if velocity.x == 0 { // No dragging
targetContentOffset.pointee = CGPoint(x: round(index) * cellWidthIncludingSpacing - scrollView.contentInset.left, y: -scrollView.contentInset.top)
}
}
This code calculates the size of your cell, how many cells have already been shown and once the scroll is finished, adjust it to leave the cell centered.
Make sure you have the pagingEnabled of your collectionView in false if you want to use this code.
Also, implement UIScrollViewDelegatein your ViewController

Change UICollectionView's bounce start/end points

Normally UICollectionView starts to bounce when scrolled past it's contentSize, ie when contentOffset < 0 or contentOffset > contentSize.width for horizontal orientation.
Is it possible to change this behavior so the bounce effect starts when scrolled past let's say 10th item (when contentOffset < itemSize.Width * 10 or contentOffset > contentSize.width - (itemSize.Width * 10))?
UPDATE 1:
#OverD - thanks for pointing me towards the right direction.
I ended up with some work in scrollViewWillEndDragging and adjusting targetContentOffset when necessary.
The problem I'm still facing is that the bounce animation is not smooth like the original bounce when reaching the contenSize end.
Any ideas what's missing? Code snippet below:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let numberOfBouncingCells = 10
let extraSectionWidth = (collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing) * numberOfBouncingCells
let startXOffset = extraSectionWidth
let endXOffset = collectionView.contentSize.width - 2 * extraSectionWidth
let yOffset = collectionView.contentOffset.y
if targetContentOffset.pointee.x < startXOffset {
targetContentOffset.pointee = CGPoint(x: startXOffset, y: yOffset)
} else if targetContentOffset.pointee.x > endXOffset {
targetContentOffset.pointee = CGPoint(x: endXOffset, y: yOffset)
}
}
UPDATE 2 (for answer):
See answer below, I ditched the scrollViewWillEndDragging approach in favor of simply changing collectionView.contentInset [.left, .right]
I've answered my own question eventually.
I only had to set collectionView.contentInset [.left, .right] to achieve expected behavior.
var collectionViewFlowLayout: UICollectionViewFlowLayout {
return collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
let numberOfBouncingCells = 10
let extraBouncingWidth = (collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing) * numberOfBouncingCells
collectionView.contentInset.left = -extraBouncingWidth
collectionView.contentInset.right = -extraBouncingWidth

How to prevent scrolling left to right of collection view in horizontal scroll direction?

I made a UICollectionView with a horizontal scroll.
I want to scroll only one direction i.e right to left my cell view size is as full view. once I scrolling cell, it should not scroll left to right.
Please Try this,
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let row = scrollView.contentOffset.x / cellWidth
currentIndexShown = Int(row)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x < cellWidth * CGFloat(currentIndexShown){
scrollView.contentOffset = CGPoint(x: cellWidth * CGFloat(currentIndexShown), y: -20)
scrollView.bounces = false
} else {
scrollView.bounces = true
}
}

UICollectionView's scrollToItem not calling targetContentOffset

I have a collection view that implements paging that is why I override the targetContentOffset in a custom UICollectionViewFlowLayout to handle it. It gets called when the collection view is scrolled through user interaction and it works. However, it does not get called when using scrollToItem or the scroll to visible rect. What would be the best way to scroll to the collection view programmatically that will surely go through the method targetContentOffset?
In my case i have used bellow method for Maintain paging and scrolling programmatically.
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if scrollView.contentOffset.y > -topHeaderView.frame.size.height && scrollView.contentOffset.y < -20 - 50 {
let aHeaderHeight = topHeaderView.frame.size.height
if velocity.y <= 0 {
targetContentOffset.memory = CGPoint(x: 0, y: -aHeaderHeight)
} else {
targetContentOffset.memory = CGPoint(x: 0, y: 20 + 50)
}
}
}
Hope this will help you.

How Can I Animate the Size of A UICollectionViewCell on Scroll so the middle one is largest?

I have a UICollectionView that shows several rows with one, full-width column (looks like a UITableView)
What I'd like to achieve is something similar to this:
... where the middle cell has a much greater height. As the user scrolls up and down, the cells before and after the middle cell animate back to the default height for the given cell.
Can somebody outline how I should approach this problem?
I use this Swift 3 code in my horizontal UICollectionView.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let centerX = scrollView.contentOffset.x + scrollView.frame.size.width/2
for cell in mainCollectionView.visibleCells {
var offsetX = centerX - cell.center.x
if offsetX < 0 {
offsetX *= -1
}
cell.transform = CGAffineTransform(scaleX: 1, y: 1)
if offsetX > 50 {
let offsetPercentage = (offsetX - 50) / view.bounds.width
var scaleX = 1-offsetPercentage
if scaleX < 0.8 {
scaleX = 0.8
}
cell.transform = CGAffineTransform(scaleX: scaleX, y: scaleX)
}
}
}
You need to create a custom subclass of UICollectionViewLayout.
First of all, override - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return yes, that way, you can change the layout attributes of your cells as your collection is being scrolled.
After that, your key methods to override are:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
and
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
I suggest reading an article about custom collection view layouts. It can be pretty heavy subject matter.
since the UICollectionView is the subclass of UIScrollView, you can solved this problem by treating your collectionView as a scrollView.
set the UIScrollViewDelegate and implemented scrollViewDidScroll, and then do something like this:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (int i = 0; i < [scrollView.subviews count]; i++) {
UIView *cell = [scrollView.subviews objectAtIndex:i];
float position = cell.center.y - scrollView.contentOffset.y;
float offset = 1.5 - (fabs(scrollView.center.y - position) * 1.0) / scrollView.center.y;
if (offset<1.0)
{
offset=1.0;
}
cell.transform = CGAffineTransformIdentity;
cell.transform = CGAffineTransformScale(cell.transform, offset, offset);
}
}
hope this will solve your problem.

Resources