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

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?

Related

Centered paging on vertical UICollectionView with custom cell height

I have seen a lot of posts where this is solved horizontally, however, I am having trouble implementing a solution for a vertical collection view.
My collection view's cells fill the entire width but not the entire height of the collection view, so normal paging does not work. I am trying to snap the cells center to the screens center when scrolling using a custom UICollectionViewFlowLayout.
(Similar to an Instagram feed but no "free" scrolling and the posts get centered vertically)
class FeedLayout: UICollectionViewFlowLayout {
private var previousOffset: CGFloat = 0
private var currentPage: Int = 0
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
let itemsCount = collectionView.numberOfItems(inSection: 0)
if previousOffset > collectionView.contentOffset.y {
currentPage = max(currentPage - 1, 0)
} else if previousOffset < collectionView.contentOffset.y {
currentPage = min(currentPage + 1, itemsCount - 1)
}
let updatedOffset = ((collectionView.frame.height * 0.75) + minimumLineSpacing) * CGFloat(currentPage)
previousOffset = updatedOffset
return CGPoint(x: proposedContentOffset.x, y: updatedOffset)
}
}
I wrote an open-source extension that does this a few days ago.
I would adjust it to center the cell (instead of scroll cell-by-cell with new cells on the top) with this changes:
public extension UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// page height used for estimating and calculating paging
let pageHeight = self.itemSize.height + self.minimumLineSpacing
// determine total pages
// collectionView adds an extra self.minimumLineSpacing to the total contentSize.height so this must be removed to get an even division of pages
let totalPages = (collectionView.contentSize.height - self.minimumLineSpacing) / pageHeight
// determine current page index
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = self.itemSize.height * 2 > collectionView.visibleSize.height ? CGPoint(x: visibleRect.midX, y: visibleRect.midY) : CGPoint(x: visibleRect.midX, y: visibleRect.midY - (self.itemSize.height / 3))
let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint)?.row ?? 0
let currentIndex = CGFloat(visibleIndexPath)
// make an estimation of the current page position
let approximatePage = collectionView.contentOffset.y / pageHeight
// determine the current page based on velocity
let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))
// create custom flickVelocity
let flickVelocity = velocity.y * 0.5
// check how many pages the user flicked, if <= 1 then flickedPages should return 0
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// determine the new vertical offset
// scroll to top of next/previos cell
// let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top
// scroll to center of next/previous cell
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) + ((collectionView.visibleSize.height - pageHeight) / 2) - collectionView.contentInset.top
// determine up or down swipe
let swipeDirection: CGFloat = flickVelocity > 0 ? 1 : -1
// determine if we are at the end of beginning of list
let beyond = newVerticalOffset + pageHeight >= collectionView.contentSize.height || collectionView.contentOffset.y < 0 ? true : false
// determine if the flick was too small to switch pages
let stay = abs(newVerticalOffset - collectionView.contentOffset.y) < (self.itemSize.height * 0.4) ? true : false
// determine if there are multiple pages available to swipe based on current page
var multipleAvailable = false
if flickVelocity > 0 {
multipleAvailable = currentIndex + swipeDirection < totalPages - 1 ? true : false
} else {
multipleAvailable = currentIndex + swipeDirection > 0 ? true : false
}
// give haptic feedback based on how many cells are scrolled
if beyond == false && stay == false {
if abs(flickedPages) > 1 && multipleAvailable {
TapticGenerator.notification(.success)
} else {
TapticGenerator.impact(.medium)
}
}
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset - collectionView.safeAreaInsets.top)
}
}

Limit UIPanGestureRecognizer to boundaries swift

I have a images inside uicollectionview cell which scroll horizontally, I want to achieve a feature like the facebook and photo app apple, you click on image and it covers the whole screen. You can pinch and pan the image, I want to add certain limitations same like the facebook and photo app, like when you pinch the picture you can pan maximum to its width.
I want the image to recenter again if user try to move image out of the boundaries. I am adding some screenshots to give idea about it.
Right now I am using the simple code.
guard gestureRecognizer.view != nil else {return}
print(self.imgView.frame)
if self.imgView.frame.size.width < (self.imgOrignal.width+100) {
return
}
let piece = gestureRecognizer.view!
// Get the changes in the X and Y directions relative to
// the superview's coordinate space.
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialCenter = piece.center
}
// Update the position for the .began, .changed, and .ended states
if gestureRecognizer.state != .cancelled {
// Add the X and Y translation to the view's original position.
let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
piece.center = newCenter
}
else {
// On cancellation, return the piece to its original location.
piece.center = initialCenter
}
}
I have resolved this issue by using UIScrollView zoom in and out, instead of using pinch and pan gesture, If any one wants to implement that functionality i am adding my code below.
func setZoomScale() {
let widthScale = self.bgView.frame.size.width / self.imgView.bounds.width
let heightScale = self.bgView.frame.size.height / self.imgView.bounds.height
let minScale = min(widthScale, heightScale)
self.imgScrollView.minimumZoomScale = minScale
self.imgScrollView.zoomScale = minScale
}
extension YOUR CLASS: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imgView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageViewSize = self.imgView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalInset = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalInset = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
}
}

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

How can I apply a CGAffineTransform to a UICollectionViewCell while the UICollectionView is scrolling

I am creating a kind of horizontally scrolling menu with multiple items that the user can scroll through.(see picture at the bottom for a preview of what I mean)
The UICollectionView offset already always centers on one of its items. What I want to do is that when an item is the next one to approach the center, I want to apply a transformation to make it larger. This is the code that I'm using to achieve this (the logic doesn't handle animating out of the center or scrolling the other direction yet):
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let scrolledCollectionView = scrollView as? UICollectionView,
let flowLayout = scrolledCollectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
let itemWidth = flowLayout.itemSize.width
let collectionViewCenter = collectionView.bounds.width * 0.5 + scrolledCollectionView.contentOffset.x
let itemToEnlarge = Int((scrolledCollectionView.contentOffset.x + (itemWidth * 0.5)) / (itemWidth + flowLayout.minimumInteritemSpacing))
let itemEnlargeIndexpath = IndexPath(row: itemToEnlarge, section: 0)
guard let cellToAnimate = collectionView.cellForItem(at: itemEnlargeIndexpath) else { return }
let diff = cellToAnimate.center.x - collectionViewCenter
var transformationVolume: CGFloat = 1
if diff == 0 {
transformationVolume += 0.2
} else {
transformationVolume += (0.2 / diff)
}
DispatchQueue.main.async {
cellToAnimate.transform = CGAffineTransform(scaleX: transformationVolume, y: transformationVolume)
}
}
The problem that I'm having is that the transformation is only applied once the collectionview has stopped scrolling. Does anyone know if there's a way to apply the transformation dynamically? So that if you scroll the item towards the center little by little, the transformation is applied incrementally.

targetContentOffsetForProposedContentOffset - wrong proposed offset

I have a custom UICollectionViewLayout and it implements targetContentOffsetForProposedContentOffset in order to set paging. The center item in the UICollectionView is the full "large" size, while each other item has a CGAffineTransformScale of the "shrunk" scale value.
My problem is that there appears to be an upper limit on the contentOffset so that I can only scroll to item 5 of 7, and it bounces back. Specifics after the code:
I'm setting the collectionViewContentSize() as follows:
#IBInspectable var shrunkScale: CGFloat = 0.5 // 0.5 in IB
#IBInspectable var largeSize: CGSize = CGSizeZero // 360x490 in IB
#IBInspectable var itemSpacing: CGFloat = 0 // 32 in IB
var widthPerAdditionalItem: CGFloat {
return largeSize.width * shrunkScale + itemSpacing
}
override func collectionViewContentSize() -> CGSize {
guard let collectionView = self.collectionView else {
return CGSizeZero
}
let count = CGFloat(collectionView.numberOfItemsInSection(0))
let width = largeSize.width + (count) * widthPerAdditionalItem
let height = collectionView.bounds.height
let size = CGSize(width: width, height: height)
return size
}
the targetOffset... methods reference a single helper method:
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint) -> CGPoint {
let closestPlace = round(proposedContentOffset.x / widthPerAdditionalItem)
guard let offsetX = offsetXForItemAtIndex(Int(closestPlace)) else {
return proposedContentOffset
}
print("Calculated: \(offsetX), Proposed: \(proposedContentOffset.x), ContentWidth: \(collectionView?.contentSize.width ?? 0 )")
return CGPoint(x: offsetX, y: proposedContentOffset.y)
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
return targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
func contentOffsetForItemAtIndexPath(indexPath: NSIndexPath) -> CGPoint? {
guard
let collectionView = self.collectionView,
let offsetX = offsetXForItemAtIndex(indexPath.item)
else {
return nil
}
print("Tap Offset: - \(offsetX) vs. \(collectionView.contentOffset.x)")
return CGPoint(x: offsetX, y: collectionView.contentOffset.y)
}
private func offsetXForItemAtIndex(index: Int) -> CGFloat? {
guard
let count = collectionView?.numberOfItemsInSection(0)
else {
return nil
}
let proposed = CGFloat(index) * widthPerAdditionalItem
let maximum = CGFloat(count) * widthPerAdditionalItem
// bound min = 0, max = count*width
return max( min(maximum, proposed), 0)
}
Here's What I get:
My content Width is 1844.0
I finish dragging the view at offset.x = 1187.5
The targetContentOffsetForProposedContentOffset receives a proposed offset.x = 820.0
I return the "paged" offset.x value of 848.0
The collectionView scrolls to offset.x of 820.0
What I am expecting:
My content Width is 1844.0
I finish dragging the view at offset.x = 1187.5
The targetContentOffsetForProposedContentOffset receives a
proposed offset.x = 1187.5
I return the "paged" offset.x value of 1272.0
The collectionView scrolls to offset.x of 1272.0
Debugging:
If I manually call setContentOffset with the calculated offset of 1272.0 then it scrolls to the correct position. But the instant I try to scroll it snaps back to 820.0
After sitting down and doing some math I figured out the following:
contentWidth = 1844
poposedContentOffset.x = 820
1844 - 820 = 1024 // <--- Width of the screen.
The content was being displayed from the center of the screen for the first item, to the center of the screen for the second item.
This means I needed to add half the collectionView's frame.width so the first item can be centered, and half again so the final item could be centered.
The collectionViewContentSize() now returns
let width = (count-1) * widthPerAdditionalItem + collectionView.bounds.width
let height = collectionView.bounds.height
let size = CGSize(width: width, height: height)

Resources