Why collectionView cell centering works only in one direction? - ios

I'm trying to centerelize my cells on horizontal scroll. I've written one method, but it works only when I scroll to right, on scroll on left it just scrolls, without stopping on the cell's center.
Can anyone help me to define this bug, please?
class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5;
let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;
if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.Cell {
continue
}
if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) {
continue
}
// == First time in the loop == //
guard let candAttrs = candidateAttributes else {
candidateAttributes = attributes
continue
}
let a = attributes.center.x - proposedContentOffsetCenterX
let b = candAttrs.center.x - proposedContentOffsetCenterX
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
if(proposedContentOffset.x == -(cv.contentInset.left)) {
return proposedContentOffset
}
return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
}
} else {
print("else")
}
// fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
And in my UIViewController:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var insets = self.collectionView.contentInset
let value = ((self.view.frame.size.width - ((CGRectGetWidth(collectionView.frame) - 35))) * 0.5)
insets.left = value
insets.right = value
self.collectionView.contentInset = insets
self.collectionView.decelerationRate = UIScrollViewDecelerationRateNormal
}
If you have any question - please ask me

I actually just did a slightly different implementation for another thread, but adjusted it to work for this questions. Try out the solution below :)
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
// Tracks the currently visible index
private var visibleIndex : Int = 0
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enough in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}

Related

custom collection view not scaling last image

so I have a custom collectionviewflowlayout that works perfectly but in one of my collection views it is not scaling the image as desired. I have attached some images below, you can see that the last cell is not scaling properly.
I'm not sure why this scaling issue is happening in this cell.
the collectionview is a custom class of collectionviewflowlayout which the snapping of cells to the centre of the screen:
class VerticalCollectionViewFlow: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfHeight = cvBounds.size.height * 0.5
print(proposedContentOffset.y)
let proposedContentOffsetCenterY = proposedContentOffset.y + halfHeight
if let attributesForVisibleCells = self.layoutAttributesForElements(in: cvBounds) {
var candidateAttributes : UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.cell {
continue
}
if (attributes.center.y == 0) || (attributes.center.y > (cv.contentOffset.y + halfHeight) && velocity.y < 0) {
continue
}
// == First time in the loop == //
guard let candAttrs = candidateAttributes else {
candidateAttributes = attributes
continue
}
let a = attributes.center.y - proposedContentOffsetCenterY
let b = candAttrs.center.y - proposedContentOffsetCenterY
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
// Beautification step , I don't know why it works!
if(proposedContentOffset.y == -(cv.contentInset.top)) {
return proposedContentOffset
}
if candidateAttributes != nil {
return CGPoint(x: proposedContentOffset.x, y: floor(candidateAttributes!.center.y - halfHeight))
}
}
}
// fallback
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
and the scaling is handled from another class that is called from viewdidload of the main viewcontroller where the delegates of UIcollectionViewController are set.
import UIKit
public enum SC_ScaledPattern {
case horizontalCenter
case horizontalLeft
case horizontalRight
case verticalCenter
case verticalBottom
case verticalTop
}
open class ScaledVisibleCellsCollectionView {
static let sharedInstance = ScaledVisibleCellsCollectionView()
var maxScale: CGFloat = 1.0
var minScale: CGFloat = 0.5
var maxAlpha: CGFloat = 1.0
var minAlpha: CGFloat = 0.5
var scaledPattern: SC_ScaledPattern = .verticalCenter
}
extension UICollectionView {
/**
Please always set
*/
public func setScaledDesginParam(scaledPattern pattern: SC_ScaledPattern, maxScale: CGFloat, minScale: CGFloat, maxAlpha: CGFloat, minAlpha: CGFloat) {
ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern = pattern
ScaledVisibleCellsCollectionView.sharedInstance.maxScale = maxScale
ScaledVisibleCellsCollectionView.sharedInstance.minScale = minScale
ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha = maxAlpha
ScaledVisibleCellsCollectionView.sharedInstance.minAlpha = minAlpha
}
/**
Please call at any time
*/
public func scaledVisibleCells() {
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter, .horizontalLeft, .horizontalRight:
scaleCellsForHorizontalScroll(visibleCells)
break
case .verticalCenter, .verticalTop, .verticalBottom:
self.scaleCellsForVerticalScroll(visibleCells)
break
}
}
}
extension UICollectionView {
fileprivate func scaleCellsForHorizontalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaWidth = bounds.width / 2
let maximumScalingAreaWidth = (bounds.width / 2 - scalingAreaWidth) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .horizontalCenter:
distanceFromMainPosition = horizontalCenter(cell)
break
case .horizontalLeft:
distanceFromMainPosition = abs(cell.frame.midX - contentOffset.x - (cell.bounds.width / 2))
break
case .horizontalRight:
distanceFromMainPosition = abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x) + (cell.bounds.width / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaWidth, scalingArea: scalingAreaWidth)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCellsForVerticalScroll(_ visibleCells: [UICollectionViewCell]) {
let scalingAreaHeight = bounds.height / 2
let maximumScalingAreaHeight = (bounds.height / 2 - scalingAreaHeight) / 2
for cell in visibleCells {
var distanceFromMainPosition: CGFloat = 0
switch ScaledVisibleCellsCollectionView.sharedInstance.scaledPattern {
case .verticalCenter:
distanceFromMainPosition = verticalCenter(cell)
break
case .verticalBottom:
distanceFromMainPosition = abs(bounds.height - (cell.frame.midY - contentOffset.y + (cell.bounds.height / 2)))
break
case .verticalTop:
distanceFromMainPosition = abs(cell.frame.midY - contentOffset.y - (cell.bounds.height / 2))
break
default:
return
}
let preferredAry = scaleCells(distanceFromMainPosition, maximumScalingArea: maximumScalingAreaHeight, scalingArea: scalingAreaHeight)
let preferredScale = preferredAry[0]
let preferredAlpha = preferredAry[1]
cell.transform = CGAffineTransform(scaleX: preferredScale, y: preferredScale)
cell.alpha = preferredAlpha
}
}
fileprivate func scaleCells(_ distanceFromMainPosition: CGFloat, maximumScalingArea: CGFloat, scalingArea: CGFloat) -> [CGFloat] {
var preferredScale: CGFloat = 0.0
var preferredAlpha: CGFloat = 0.0
let maxScale = ScaledVisibleCellsCollectionView.sharedInstance.maxScale
let minScale = ScaledVisibleCellsCollectionView.sharedInstance.minScale
let maxAlpha = ScaledVisibleCellsCollectionView.sharedInstance.maxAlpha
let minAlpha = ScaledVisibleCellsCollectionView.sharedInstance.minAlpha
if distanceFromMainPosition < maximumScalingArea {
// cell in maximum-scaling area
preferredScale = maxScale
preferredAlpha = maxAlpha
} else if distanceFromMainPosition < (maximumScalingArea + scalingArea) {
// cell in scaling area
let multiplier = abs((distanceFromMainPosition - maximumScalingArea) / scalingArea)
preferredScale = maxScale - multiplier * (maxScale - minScale)
preferredAlpha = maxAlpha - multiplier * (maxAlpha - minAlpha)
} else {
// cell in minimum-scaling area
preferredScale = minScale
preferredAlpha = minAlpha
}
return [ preferredScale, preferredAlpha ]
}
}
extension UICollectionView {
fileprivate func horizontalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.width / 2 - (cell.frame.midX - contentOffset.x))
}
fileprivate func verticalCenter(_ cell: UICollectionViewCell)-> CGFloat {
return abs(bounds.height / 2 - (cell.frame.midY - contentOffset.y))
}
}
I have this working on multi collection views, just this one is producing the scaling error.
below is the extension of the collectionview that is handling the sticky nature of the collection/scrollview
extension JourneyViewController: UIScrollViewDelegate {
//------------------------------------------------------------------ make the scrolling a bit sticky
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.collectionViewPart.scaledVisibleCells()
let offsetY = scrollView.contentOffset.x
let contentHeight = scrollView.contentSize.width
if offsetY > contentHeight - scrollView.frame.size.width {
self.collectionViewPart.reloadData()
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startingScrollingOffset = scrollView.contentOffset
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = collectionViewPart.collectionViewLayout.collectionViewContentSize.width
let page: CGFloat
let proposedPage = scrollView.contentOffset.x / cellWidth
// 10% of next cell to change / 90% of previous cell to change
let delta: CGFloat = scrollView.contentOffset.x > startingScrollingOffset.x ? 0.1 : 0.9
if floor(proposedPage + delta) == floor(proposedPage) {
page = floor(proposedPage)
}
else {
page = floor(proposedPage + 1)
}
}
}
hope someone smarter than me can figure this out I've been staring at it for countless hours.
thanks!

IOS swift - objective c code migration to swift

I am trying to convert this code in swift.
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGSize collectionViewSize = self.collectionView.bounds.size;
CGFloat proposedContentOffsetCenterX = proposedContentOffset.x + self.collectionView.bounds.size.width * 0.5f;
CGRect proposedRect = self.collectionView.bounds;
// Comment out if you want the collectionview simply stop at the center of an item while scrolling freely
// proposedRect = CGRectMake(proposedContentOffset.x, 0.0, collectionViewSize.width, collectionViewSize.height);
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in [self layoutAttributesForElementsInRect:proposedRect])
{
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory != UICollectionElementCategoryCell)
{
continue;
}
// == First time in the loop == //
if(!candidateAttributes)
{
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) < fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX))
{
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - self.collectionView.bounds.size.width * 0.5f, proposedContentOffset.y);
}
This is what I have done so far
func targetContentOffsetForProposedContentOffset(proposedContentOffset:CGPoint, velocity: CGPoint) -> CGPoint
{
let collectionViewSize:CGSize = self.collectionView.bounds.size
let proposedContentOffsetCenterX:CGFloat = proposedContentOffset.x + self.collectionView.bounds.size.width * 0.5
let proposedRect:CGRect = self.collectionView.bounds
//let candidateAttributes:UICollectionViewLayoutAttributes
var candidateAttributes:[UICollectionViewLayoutAttributes]
/* get stuck here in for loop*/
for (attributes: [UICollectionViewLayoutAttributes] in layoutAttributesForElementsInRect:proposedRect)
{
}
The issue is layoutAttributesForElementsInRect is a function and proposedRect is the argument of that function. You are calling that function wrongly in your code.
You need to change:
for (attributes: [UICollectionViewLayoutAttributes] in layoutAttributesForElementsInRect:proposedRect)
to
for attributes in layoutAttributesForElementsInRect(proposedRect) as! [UICollectionViewLayoutAttributes]
This solution may help you.
for attributes: UICollectionViewLayoutAttributes in self.layoutAttributesForElementsInRect(proposedRect)
{
// ADD YOUR CODE HERE
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return .zero
}
let collectionViewSize = collectionView.bounds.size
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width / 2
let proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
var candidateAttributes: UICollectionViewLayoutAttributes?
guard let layoutAttributes = layoutAttributesForElements(in: proposedRect) else {
return .zero
}
for attributes in layoutAttributes {
if attributes.representedElementCategory != UICollectionView.ElementCategory.cell {
continue
}
if candidateAttributes == nil {
candidateAttributes = attributes
continue
}
guard let unwrappedCandidateAttributes = candidateAttributes else {
return .zero
}
if abs(attributes.center.x - proposedContentOffsetCenterX) < abs(unwrappedCandidateAttributes.center.x - proposedContentOffsetCenterX) {
candidateAttributes = attributes
}
}
guard let unwrappedCandidateAttributes = candidateAttributes else {
return .zero
}
var newProposedContentOffset = proposedContentOffset
newProposedContentOffset.x = unwrappedCandidateAttributes.center.x - collectionView.bounds.size.width / 2
let offset = proposedContentOffset.x - collectionView.contentOffset.x
if (velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0) {
let pageWidth = itemSize.width + minimumLineSpacing
newProposedContentOffset.x += velocity.x > 0 ? pageWidth : -pageWidth;
}
return newProposedContentOffset
}

Circular wheel animation implementation in ios

I have to do a animation like :
https://www.flickr.com/photos/134104584#N07/20409116003/in/datetaken/
Currently I am trying to achieve this using custom collection view layout and subclassing UICollectionViewLayout.
Any help or way to achieve this ?
I had the same task and that's what I've found:
You can make your own solution, following great tutorial made by Rounak Jain. The tutorial is available here:
https://www.raywenderlich.com/107687/uicollectionview-custom-layout-tutorial-spinning-wheel
You can reuse the existing code.
Because of time limits I've chosen the second variant, and the best repo I've found was:
https://github.com/sarn/ARNRouletteWheelView
The usage is pretty straightforward, but there are two tips that can be useful:
scrollToItemAtIndexPath was not working and was crashing the app. The Pull Request to fix it: https://github.com/sarn/ARNRouletteWheelView/pull/3/commits/7056e4bac2a04f2c8208d6d43e25d62a848f032d
If you want to make items smaller and disable rotation of the item itself (see gif), you can do the following in your UICollectionViewCell subclass:
#import "ARNRouletteWheelCellScaledAndRotated.h"
#import <ARNRouletteWheelView/ARNRouletteWheelLayoutAttributes.h>
#implementation ARNRouletteWheelCellScaledAndRotated
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
if(layoutAttributes != nil && [layoutAttributes isKindOfClass:[ARNRouletteWheelLayoutAttributes class]]) {
ARNRouletteWheelLayoutAttributes *rouletteWheelLayoutAttributes = (ARNRouletteWheelLayoutAttributes *)layoutAttributes;
self.layer.anchorPoint = rouletteWheelLayoutAttributes.anchorPoint;
// update the Y center to reflect the anchor point
CGPoint center = CGPointMake(self.center.x, self.center.y + ((rouletteWheelLayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)));
self.center = center;
// rotate back
CGAffineTransform rotate = CGAffineTransformMakeRotation(-rouletteWheelLayoutAttributes.angle);
// scale
CGFloat scale = 1 - fabs(rouletteWheelLayoutAttributes.angle);
CGAffineTransform translate = CGAffineTransformMakeScale(scale, scale);
// Apply them to a view
self.imageView.transform = CGAffineTransformConcat(translate, rotate);
}
}
#end
Update this tutorial - https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel. Old version not work. This version good work for Swift 5 + i add pageControl if somebody need.
import UIKit
class CircularCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
//1
var anchorPoint = CGPoint(x: 0.5, y: 0.5)
var angle: CGFloat = 0 {
//2
didSet {
zIndex = Int(angle * 1000000)
transform = CGAffineTransform.identity.rotated(by: angle)
}
}
override func copy(with zone: NSZone? = nil) -> Any {
let copiedAttributes: CircularCollectionViewLayoutAttributes = super.copy(with: zone) as! CircularCollectionViewLayoutAttributes
copiedAttributes.anchorPoint = self.anchorPoint
copiedAttributes.angle = self.angle
return copiedAttributes
}
}
protocol CircularViewLayoutDelegate: AnyObject {
func getCurrentIndex(value: Int)
}
class CircularCollectionViewLayout: UICollectionViewLayout {
let itemSize = CGSize(width: 214, height: 339)
let spaceBeetweenItems: CGFloat = 50
var angleAtExtreme: CGFloat {
return collectionView!.numberOfItems(inSection: 0) > 0 ?
-CGFloat(collectionView!.numberOfItems(inSection: 0)) * anglePerItem : 0
}
var angle: CGFloat {
return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - itemSize.width)
}
var radius: CGFloat = 1000 {
didSet {
invalidateLayout()
}
}
var anglePerItem: CGFloat {
return atan((itemSize.width + spaceBeetweenItems) / radius)
}
weak var delegate: CircularViewLayoutDelegate?
override var collectionViewContentSize: CGSize {
return CGSize(width: (CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width) + 100, height: collectionView!.bounds.height)
}
override class var layoutAttributesClass: AnyClass {
return CircularCollectionViewLayoutAttributes.self
}
var attributesList = [CircularCollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2.0
let anchorPointY = (itemSize.height + radius) / itemSize.height
// Add to get current cell index
let theta = atan2(collectionView.bounds.width / 2.0,
radius + (itemSize.height / 2.0) - collectionView.bounds.height / 2.0)
let endIndex = collectionView.numberOfItems(inSection: 0)
let currentIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
delegate?.getCurrentIndex(value: currentIndex)
attributesList = (0..<collectionView.numberOfItems(inSection: 0)).map {
(i) -> CircularCollectionViewLayoutAttributes in
//1
let attributes = CircularCollectionViewLayoutAttributes.init(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
//2
attributes.center = CGPoint(x: centerX, y: (self.collectionView!.bounds).midY)
//3
attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))
attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)
return attributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// return an array layout attributes instances for all the views in the given rect
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return attributesList[indexPath.row]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Function in Cell Class:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
let circularlayoutAttributes = layoutAttributes as! CircularCollectionViewLayoutAttributes
self.layer.anchorPoint = circularlayoutAttributes.anchorPoint
self.center.y += (circularlayoutAttributes.anchorPoint.y - 0.5) * self.bounds.height
}
Add this to controller if you need current cell to add pageControl:
In ViewDidload:
if let layout = collectionView?.collectionViewLayout as? CircularCollectionViewLayout {
layout.delegate = self
}
Extensions:
extension LevelsVC: CircularViewLayoutDelegate {
func getCurrentIndex(value: Int) {
self.pageControl.currentPage = value - 1
}
}

Get the indexPath of cell in the center of collectionView in swift?

I have tried this to get the index of a cell in the center of a collection view by using an extension of uicollectionview, but I always get nil instead of the index. How can I fix this?
extension UICollectionView {
var centerPoint : CGPoint {
get {
return CGPoint(x: self.center.x + self.contentOffset.x, y: self.center.y + self.contentOffset.y);
}
}
var centerCellIndexPath: NSIndexPath? {
if let centerIndexPath: NSIndexPath = self.indexPathForItemAtPoint(self.centerPoint) {
return centerIndexPath
}
return nil
}
}
then: in a UIViewController in a random method I have this:
if let centerCellIndexPath: NSIndexPath = collectionTemp!.centerCellIndexPath {
print(centerCellIndexPath)
} else {
println("nil")
}
the index path is always nil, and I don't get it why, because the cells display in order, everything is fine except this.
I've managed to solve my problem by using a custom layout that always keeps a cell in the center. Using this the indexPath in the center can never be nil.
class CenterFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5
let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth
if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) as [UICollectionViewLayoutAttributes]! {
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.Cell {
continue
}
if let candAttrs = candidateAttributes {
let a = attributes.center.x - proposedContentOffsetCenterX
let b = candAttrs.center.x - proposedContentOffsetCenterX
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes
}
} else { // == First time in the loop == //
candidateAttributes = attributes
continue
}
}
return CGPoint(x : candidateAttributes!.center.x - halfWidth, y : proposedContentOffset.y)
}
}
// Fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
Add this as the subclass of the UICollectionFlowLayout.

UICollectionView Cell Scroll to centre

I am using UICollectionView in my UIViewController.
My collectionview properties are set as below.
Now I would like cell to be Centre on screen after scroll!
Option 1:
Option 2:
What would I have to do achieve option 2?
UPDATE:
In the end I have used following code as scrolling with other answer is not smooth.
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{ CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray* array = [super layoutAttributesForElementsInRect:targetRect];
for (UICollectionViewLayoutAttributes* layoutAttributes in array) {
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
You can override targetContentOffsetForProposedContentOffset:withScrollingVelocity: method in your UICollectionViewLayout subclass and calculate your offset there like this:
#property (nonatomic, assign) CGFloat previousOffset;
#property (nonatomic, assign) NSInteger currentPage;
...
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
NSInteger itemsCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:0];
// Imitating paging behaviour
// Check previous offset and scroll direction
if ((self.previousOffset > self.collectionView.contentOffset.x) && (velocity.x < 0.0f)) {
self.currentPage = MAX(self.currentPage - 1, 0);
} else if ((self.previousOffset < self.collectionView.contentOffset.x) && (velocity.x > 0.0f)) {
self.currentPage = MIN(self.currentPage + 1, itemsCount - 1);
}
// Update offset by using item size + spacing
CGFloat updatedOffset = (self.itemSize.width + self.minimumInteritemSpacing) * self.currentPage;
self.previousOffset = updatedOffset;
return CGPointMake(updatedOffset, proposedContentOffset.y);
}
EDIT: thanks for pointing this out, forgot to say that you have to disable paging first:
self.collectionView.pagingEnabled = NO;
UPDATE: attaching Swift 4.2 version
...
collectionView.isPagingEnabled = false
...
class YourCollectionLayoutSubclass: 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)
// Imitating paging behaviour
// Check previous offset and scroll direction
if previousOffset > collectionView.contentOffset.x && velocity.x < 0 {
currentPage = max(currentPage - 1, 0)
} else if previousOffset < collectionView.contentOffset.x && velocity.x > 0 {
currentPage = min(currentPage + 1, itemsCount - 1)
}
// Update offset by using item size + spacing
let updatedOffset = (itemSize.width + minimumInteritemSpacing) * CGFloat(currentPage)
previousOffset = updatedOffset
return CGPoint(x: updatedOffset, y: proposedContentOffset.y)
}
}
you can use code
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
Here's a Swift 3 version of #dmitry-zhukov (thanks btw!)
class PagedCollectionLayout : UICollectionViewFlowLayout {
var previousOffset : CGFloat = 0
var currentPage : CGFloat = 0
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let sup = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
guard
let validCollection = collectionView,
let dataSource = validCollection.dataSource
else { return sup }
let itemsCount = dataSource.collectionView(validCollection, numberOfItemsInSection: 0)
// Imitating paging behaviour
// Check previous offset and scroll direction
if (previousOffset > validCollection.contentOffset.x) && (velocity.x < 0) {
currentPage = max(currentPage - 1, 0)
}
else if (previousOffset < validCollection.contentOffset.x) && (velocity.x > 0) {
currentPage = min(currentPage + 1, CGFloat(itemsCount - 1))
}
// Update offset by using item size + spacing
let updatedOffset = ((itemSize.width + minimumInteritemSpacing) * currentPage)
self.previousOffset = updatedOffset
let updatedPoint = CGPoint(x: updatedOffset, y: proposedContentOffset.y)
return updatedPoint
}
}
I have found a lot of information and solutions.
now, I use this.
on UICollectionViewFlowLayout override:
override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if display != .inline {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
let willtoNextX: CGFloat
if proposedContentOffset.x <= 0 || collectionView.contentOffset == proposedContentOffset {
willtoNextX = proposedContentOffset.x
} else {
let width = collectionView.bounds.size.width
willtoNextX = collectionView.contentOffset.x + (velocity.x > 0 ? width : -width)
}
let targetRect = CGRect(x: willtoNextX, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
var offsetAdjustCoefficient = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left
let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
layoutAttributesArray?.forEach({ (layoutAttributes) in
let itemOffset = layoutAttributes.frame.origin.x
if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustCoefficient)) {
offsetAdjustCoefficient = itemOffset - horizontalOffset
}
})
return CGPoint(x: proposedContentOffset.x + offsetAdjustCoefficient, y: proposedContentOffset.y)
}
and on UICollectionViewController:
collectionView.decelerationRate = .fast
collectionView.isPagingEnabled = false
collectionView.contentInset = UIEdgeInsets.init(top: 0, left: 16, bottom: 0, right: 16)
now, cell is in center!!
preview
After setting proper itemSize, left and right insets I prefer doing this rather than subclassing layout
//Setting decelerationRate to fast gives a nice experience
collectionView.decelerationRate = .fast
//Add this to your view anywhere
func centerCell () {
let centerPoint = CGPoint(x: collectionView.contentOffset.x + collectionView.frame.midX, y: 100)
if let path = collectionView.indexPathForItem(at: centerPoint) {
collectionView.scrollToItem(at: path, at: .centeredHorizontally, animated: true)
}
}
//Set collectionView.delegate = self then add below funcs
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
centerCell()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
centerCell()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
centerCell()
}
}
I don't know why everybody's answer is so complicated, I simply turn on Paging enabled in Interface Builder and it works perfectly.
My solution for scrolling like paging.
//
collectionview.isPaging = false
//
class CollectionLayoutSubclass: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var point = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
guard let collectionView = collectionView else {
return point
}
let cells = collectionView.visibleCells
let centerPoint = collectionView.center
var cellFrame: CGRect = CGRect.zero
for cell in cells {
cellFrame = collectionView.convert(cell.frame, to: collectionView.superview)
var newCenterPoint: CGPoint = centerPoint
if velocity.x > 0 {
newCenterPoint = CGPoint(x: centerPoint.x * 1.5, y: centerPoint.y)
} else if velocity.x < 0 {
newCenterPoint = CGPoint(x: centerPoint.x * 0.5, y: centerPoint.y)
}
guard cellFrame.contains(newCenterPoint) else {
continue
}
let x = collectionView.frame.width / 2 - cell.frame.width / 2
point.x = cell.frame.origin.x - x
break
}
return point
}
}
Copied from an answer from another question:
Here is my implementation
func snapToNearestCell(scrollView: UIScrollView) {
let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2)
if let indexPath = self.collectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Implement your scroll view delegates like this
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
self.snapToNearestCell(scrollView: scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.snapToNearestCell(scrollView: scrollView)
}
Also, for better snapping
self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast

Resources