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

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

Related

How to achieve horizontal scrolling effect of Apple's Photos app?

If you'd go to Photos app's For You section and scroll featured photos you'd see smooth snapping effect for each cell.
I have a horizontally scrollable collection view where I want to have the same effect. So far I've managed to make it somehow similar to what I want by creating custom Collection View Layout by subclassing UICollectionViewFlowLayout and overriding this method:
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
let point = super.targetContentOffset(
forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity
)
guard let collectionView = self.collectionView else {
return point
}
let contentWidth = itemSize.width + minimumInteritemSpacing
let anchor = collectionView.contentOffset.x / contentWidth
let currentPage: CGFloat
if velocity.x == 0 {
currentPage = round(anchor)
} else if velocity.x < 0 {
currentPage = floor(anchor)
} else {
currentPage = ceil(anchor)
}
let flickVelocity = velocity.x * 0.2
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
let newHorizontalOffset = ((currentPage + flickedPages) * (itemSize.width + minimumInteritemSpacing)) -
collectionView.contentInset.left
return CGPoint(x: newHorizontalOffset, y: point.y)
}
But in reality if I scroll a bit harder it will go 2 cells forward or back: https://imgur.com/a/toVMQ53
How could I achieve strong yet smooth snapping effect (content offset) for my collection view as in Photos App?

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.

Add snap-to position in a UITableView or UIScrollView

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
}

Resources