Skip to content

Instantly share code, notes, and snippets.

@mmick66
Last active August 2, 2022 10:06
Show Gist options
  • Star 88 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save mmick66/9812223 to your computer and use it in GitHub Desktop.
Save mmick66/9812223 to your computer and use it in GitHub Desktop.
UICollectionViewFlowLayout with arbitrary sized Paging
#import "UICollectionViewFlowLayoutCenterItem.h"
@implementation UICollectionViewFlowLayoutCenterItem
- (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);
}
@end
@DavidSchechter
Copy link

Is there a way to have this and only move one page at a time like pagingEnabled?

@deepakdhanaie
Copy link

How could i make it circular or infinite with item at centre?

@shivang2902
Copy link

shivang2902 commented Mar 22, 2017

@DavidSchechter did you find any solution for your concern ????

@Nautiyalsachin
Copy link

@mcginnik I loved your answer, just converted it to Swift 3. Working perfect for me.

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    if let collectionView = collectionView,
        let first = layoutAttributesForItem(at: IndexPath(row: 0, section: 0)),
        let last = layoutAttributesForItem(at: IndexPath(row: collectionView.numberOfItems(inSection: 0) - 1, section: 0))
    {
        sectionInset = UIEdgeInsets(top: 0, left: collectionView.frame.width / 2 - first.bounds.size.width / 2, bottom: 0, right: collectionView.frame.width / 2  - last.bounds.size.width / 2)
    }
    
    let collectionViewSize = self.collectionView!.bounds.size
    let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5
    
    var proposedRect = self.collectionView!.bounds
    
    // comment this out if you don't want it to scroll so quickly
    proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
    
    var candidateAttributes: UICollectionViewLayoutAttributes?
    for attributes in self.layoutAttributesForElements(in: proposedRect)! {
        // == Skip comparison with non-cell items (headers and footers) == //
        if attributes.representedElementCategory != .cell {
            continue
        }
        
        
        // Get collectionView current scroll position
        let currentOffset = self.collectionView!.contentOffset
        
        // Don't even bother with items on opposite direction
        // You'll get at least one, or else the fallback got your back
        if (attributes.center.x <= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x >= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {
            continue
        }
        
        
        // First good item in the loop
        if candidateAttributes == nil {
            candidateAttributes = attributes
            continue
        }
        
        // Save constants to improve readability
        let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
        let centerOffset = attributes.center.x - proposedContentOffsetCenterX
        
        if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
            candidateAttributes = attributes
        }
    }
    
    if candidateAttributes != nil {
        // Great, we have a candidate
        return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
    } else {
        // Fallback
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
}

Thanks!

@iniko1983x
Copy link

Hi!
Loved the @efremidze answer.
I simple remove "paging snap", and now collection view will move on centered element due several pages:

`
class CollectionViewFlowLayoutCenterItem: UICollectionViewFlowLayout {

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
                                  withScrollingVelocity velocity: CGPoint) -> CGPoint {

	var result = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)

	guard let collectionView = collectionView else {
		return result
	}

	let halfWidth = 0.5 * collectionView.bounds.size.width
	let proposedContentCenterX = result.x + halfWidth

	let targetRect = CGRect(origin: result, size: collectionView.bounds.size)
	let layoutAttributes = layoutAttributesForElements(in: targetRect)?
		.filter { $0.representedElementCategory == .cell }
		.sorted { abs($0.center.x - proposedContentCenterX) < abs($1.center.x - proposedContentCenterX) }

	guard let closest = layoutAttributes?.first else {
		return result
	}

	result = CGPoint(x: closest.center.x - halfWidth, y: proposedContentOffset.y)
	return result
}

}
`

@J7mbo
Copy link

J7mbo commented Jan 8, 2018

@iniko1983x's solution works very nicely, although both the first element and last elements are centred instead of left and right aligned respectively

@romanfurman6
Copy link

Maybe some one need

  override func layoutSubviews() {
    super.layoutSubviews()
    if
      let first = collectionView.layoutAttributesForItem(at: IndexPath(item: 0, section: 0)),
      let last = collectionView.layoutAttributesForItem(at: IndexPath(item: collectionView.numberOfItems(inSection: 0) - 1, section: 0)) {

      let left = collectionView.frame.width / 2 - first.bounds.size.width / 2
      let right = collectionView.frame.width / 2  - last.bounds.size.width / 2
      collectionView.contentInset = UIEdgeInsets(top: 0.0, left: left, bottom: 0.0, right: right)
    }
    self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast
  }
class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {

  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    let collectionViewSize = self.collectionView!.bounds.size
    let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5

    var proposedRect = self.collectionView!.bounds

    // comment this out if you don't want it to scroll so quickly
    proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)

    var candidateAttributes: UICollectionViewLayoutAttributes?
    for attributes in self.layoutAttributesForElements(in: proposedRect)! {
      // == Skip comparison with non-cell items (headers and footers) == //
      if attributes.representedElementCategory != .cell {
        continue
      }

      // Get collectionView current scroll position
      let currentOffset = self.collectionView!.contentOffset

      // Don't even bother with items on opposite direction
      // You'll get at least one, or else the fallback got your back
      // swiftlint:disable:next line_length
      if (attributes.center.x <= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x >= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {

        continue
      }

      // First good item in the loop
      if candidateAttributes == nil {
        candidateAttributes = attributes
        continue
      }

      // Save constants to improve readability
      let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
      let centerOffset = attributes.center.x - proposedContentOffsetCenterX

      if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
        candidateAttributes = attributes
      }
    }

    if candidateAttributes != nil {
      // Great, we have a candidate
      return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
    } else {
      // Fallback
      return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
  }

}

@Iraniya
Copy link

Iraniya commented Mar 22, 2018

@DavidSchechter did you found the solution for scrolling one cell at a time using this? or anyone else?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment