UICollectionView Horizontal Paging not centered
Remove spaces between items. For horizontal scrolling collection view set minimum line spacing to 0. You can do this with interface builder or with method of UICollectionViewDelegateFlowLayout
protocol:
- (CGFloat)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}
Another way is making your cell's width less than collectionView's width for a value of horizontal space between items. Then add section insets with left and right insets that equal a half of horizontal space between items. For example, your minimum line spacing is 10:
- (CGFloat)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 10;
}
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(collectionView.frame.size.width - 10, collectionView.frame.size.height);
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 5, 0, 5);
}
And third way: manipulate collectionView scroll in scrollViewDidEndDecelerating:
method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == self.collectionView) {
CGPoint currentCellOffset = self.collectionView.contentOffset;
currentCellOffset.x += self.collectionView.frame.size.width / 2;
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:currentCellOffset];
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
animated:YES];
}
}
UICollectionView horizontal paging not centring after rotation
You don't seem to have handled rotations. You need to reloadData
when a rotation occurs. You'll get the event in UIViewController
from there you can call reloadData
on the instance of your UICollectionView
.
class GalleryViewController: UIViewController {
//...
var galleryScrollView: GalleryScrollView
//...
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
galleryScrollView.collectionViewLayout.invalidateLayout()
}
//...
}
Then in GalleryScrollView
:
class GalleryScrollView: UIView {
var currentVisibleIndexPath = IndexPath(row: 0, section: 0)
//....
}
extension GalleryScrollView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
//...
func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
let attributes = collectionView.layoutAttributesForItem(at: currentVisibleIndexPath)
let newOriginForOldIndex = attributes?.frame.origin
return newOriginForOldIndex ?? proposedContentOffset
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let center = CGPoint(x: scrollView.contentOffset.x + (scrollView.frame.width / 2), y: (scrollView.frame.height / 2))
if let indexPath = self.screenView.indexPathForItem(at: center) {
currentVisibleIndexPath = indexPath
}
}
}
UICollectionView horizontal paging with space between pages
Solution one:
collectionView.isPagingEnabled = false
- add a
minimumLineSpacing
for the distance between pages - implement
targetContentOffsetForProposedContentOffset:withScrollingVelocity:
to move the contentOffset to the closest page. You can calculate the page with simple math based on youritemSize
andminimumLineSpacing
, but it can take a little work to get it right.
Solution Two:
collectionView.isPagingEnabled = true
- add a
minimumLineSpacing
for the distance between pages - the paging size is based on the bounds of the collectionView. So make the collectionView larger then then screenSize. For example, if you have a
minimumLineSpacing
of 10 then set the frame of the collectionView to be {0,-5, width+10, height} - set a contentInset equal to the
minimumLineSpacing
to make the first and last item appear correctly.
UICollectionView align logic missing in horizontal paging scrollview
The fundamental issue is Flow Layout is not designed to support the paging. To achieve the paging effect, you will have to sacrifice the space between cells. And carefully calculate the cells frame and make it can be divided by the collection view frame without remainders. I will explain the reason.
Saying the following layout is what you wanted.
Notice, the most left margin (green) is not part of the cell spacing. It is determined by the flow layout section inset. Since flow layout doesn't support heterogeneous spacing value. It is not a trivial task.
Therefore, after setting the spacing and inset. The following layout is what you will get.
After scroll to next page. Your cells are obviously not aligned as what you expected.
Making the cell spacing 0 can solve this issue. However, it limits your design if you want the extra margin on the page, especially if the margin is different from the cell spacing. It also requires the view frame must be divisible by the cell frame. Sometimes, it is a pain if your view frame is not fixed (considering the rotation case).
The real solution is to subclass UICollectionViewFlowLayout and override following methods
- (CGSize)collectionViewContentSize
{
// Only support single section for now.
// Only support Horizontal scroll
NSUInteger count = [self.collectionView.dataSource collectionView:self.collectionView
numberOfItemsInSection:0];
CGSize canvasSize = self.collectionView.frame.size;
CGSize contentSize = canvasSize;
if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
NSUInteger page = ceilf((CGFloat)count / (CGFloat)(rowCount * columnCount));
contentSize.width = page * canvasSize.width;
}
return contentSize;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize canvasSize = self.collectionView.frame.size;
NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
CGFloat pageMarginX = (canvasSize.width - columnCount * self.itemSize.width - (columnCount > 1 ? (columnCount - 1) * self.minimumLineSpacing : 0)) / 2.0f;
CGFloat pageMarginY = (canvasSize.height - rowCount * self.itemSize.height - (rowCount > 1 ? (rowCount - 1) * self.minimumInteritemSpacing : 0)) / 2.0f;
NSUInteger page = indexPath.row / (rowCount * columnCount);
NSUInteger remainder = indexPath.row - page * (rowCount * columnCount);
NSUInteger row = remainder / columnCount;
NSUInteger column = remainder - row * columnCount;
CGRect cellFrame = CGRectZero;
cellFrame.origin.x = pageMarginX + column * (self.itemSize.width + self.minimumLineSpacing);
cellFrame.origin.y = pageMarginY + row * (self.itemSize.height + self.minimumInteritemSpacing);
cellFrame.size.width = self.itemSize.width;
cellFrame.size.height = self.itemSize.height;
if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
cellFrame.origin.x += page * canvasSize.width;
}
return cellFrame;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes * attr = [super layoutAttributesForItemAtIndexPath:indexPath];
attr.frame = [self frameForItemAtIndexPath:indexPath];
return attr;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * attrs = [NSMutableArray array];
[originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
NSIndexPath * idxPath = attr.indexPath;
CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
if (CGRectIntersectsRect(itemFrame, rect))
{
attr = [self layoutAttributesForItemAtIndexPath:idxPath];
[attrs addObject:attr];
}
}];
return attrs;
}
Notice, above code snippet only supports single section and horizontal scroll direction. But it is not hard to expand.
Also, if you don't have millions of cells. Caching those UICollectionViewLayoutAttributes may be a good idea.
UICollectionView horizontal paging with 3 items
Edit:
Demo link: https://github.com/raheelsadiq/UICollectionView-horizontal-paging-with-3-items
After a lot searching I did it, find the next point to scroll to and disable the paging. In scrollviewWillEndDragging scroll to next cell x.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
float pageWidth = 480 + 50; // width + space
float currentOffset = scrollView.contentOffset.x;
float targetOffset = targetContentOffset->x;
float newTargetOffset = 0;
if (targetOffset > currentOffset)
newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth;
else
newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth;
if (newTargetOffset < 0)
newTargetOffset = 0;
else if (newTargetOffset > scrollView.contentSize.width)
newTargetOffset = scrollView.contentSize.width;
targetContentOffset->x = currentOffset;
[scrollView setContentOffset:CGPointMake(newTargetOffset, scrollView.contentOffset.y) animated:YES];
}
I also had to make the left and right small and center large, so i did it with transform.
The issue was finding the index, so that was very difficult to find.
For transform left and right in this same method use the newTargetOffset
int index = newTargetOffset / pageWidth;
if (index == 0) { // If first index
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = CGAffineTransformIdentity;
}];
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index + 1 inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];
}else{
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = CGAffineTransformIdentity;
}];
index --; // left
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];
index ++;
index ++; // right
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];
}
And in cellForRowAtIndex add
if (indexPath.row == 0 && isfirstTimeTransform) { // make a bool and set YES initially, this check will prevent fist load transform
isfirstTimeTransform = NO;
}else{
cell.transform = TRANSFORM_CELL_VALUE; // the new cell will always be transform and without animation
}
Add these two macros too or as u wish to handle both
#define TRANSFORM_CELL_VALUE CGAffineTransformMakeScale(0.8, 0.8)
#define ANIMATION_SPEED 0.2
The end result is
Centring a cell of a horizontally scrollable collection view when paging is enabled and making other cells partly visible, too
Actually, the solution isn't very hard. Thanks to this article that shows the right way to implement this feature, I finally implemented it.
The main thing to understand is that the paging, built in a collection view, won't work in this case, because the size of cells is less than the size of the screen. So, we need to implement our custom paging technology.
First, inside of a custom helper struct
, we define a property that will hold the index of the cell before dragging :
var indexOfCellBeforeDragging: Int = 0
Then, we create a method for calculating the section inset of cells:
//Setting the inset
func calculateSectionInset(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout, numberOfCells: Int) -> CGFloat {
let inset = (collectionViewLayout.collectionView!.frame.width) / CGFloat(numberOfCells)
return inset
}
Now we can use the method above, to set the size of items of the collection view.
//Setting the item size of the collection view
func configureCollectionViewLayoutItemSize(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout) {
let inset: CGFloat = calculateSectionInset(forCollectionViewLayout: collectionViewLayout, numberOfCells: 5)
collectionViewLayout.sectionInset = UIEdgeInsets(top: 0, left: inset/4, bottom: 0, right: inset/4)
collectionViewLayout.itemSize = CGSize(width: collectionViewLayout.collectionView!.frame.size.width - inset / 2, height: collectionViewLayout.collectionView!.frame.size.height)
collectionViewLayout.collectionView?.reloadData()
}
We need to get the index of the major cell (the cell that is in the biggest right now). To do so, we use the information about items width and collection view's contentOffset
on the x
axis:
//Getting the index of the major cell
func indexOfMajorCell(in collectionViewLayout: UICollectionViewFlowLayout) -> Int {
let itemWidth = collectionViewLayout.itemSize.width
let proportionalLayout = collectionViewLayout.collectionView!.contentOffset.x / itemWidth
return Int(round(proportionalLayout))
}
Next, we set the index of cell before starting dragging, using the method above:
//Setting the index of cell before starting dragging
mutating func setIndexOfCellBeforeStartingDragging(indexOfMajorCell: Int) {
indexOfCellBeforeDragging = indexOfMajorCell
}
Now we should handle the end of the dragging. Also, we take care of the possible snapping gesture on cells. Here we use some custom threshold for the swipe velocity.
//Handling dragging end of a scroll view
func handleDraggingWillEndForScrollView(_ scrollView: UIScrollView, inside collectionViewLayout: UICollectionViewFlowLayout, withVelocity velocity: CGPoint, usingIndexOfMajorCell indexOfMajorCell: Int) {
//Calculating where scroll view should snap
let indexOfMajorCell = indexOfMajorCell
let swipeVelocityThreshold: CGFloat = 0.5
let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < 5 && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = ((indexOfCellBeforeDragging - 1) >= 0) && (velocity.x < -swipeVelocityThreshold)
let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
if didUseSwipeToSkipCell {
let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
let toValue = collectionViewLayout.itemSize.width * CGFloat(snapToIndex)
// Damping equal 1 => no oscillations => decay animation
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: {
scrollView.contentOffset = CGPoint(x: toValue, y: 0)
scrollView.layoutIfNeeded()
}, completion: nil)
} else {
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Now, in your view controller inside scrollViewWillBeginDragging(_:)
method we need to get the index of the major cell and set it as an index before starting dragging (commentsPagingHelper
is an instance of the helper struct):
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)
commentsPagingHelper.setIndexOfCellBeforeStartingDragging(indexOfMajorCell: indexOfMajorCell)
}
Finally, we handle the change of an index of the major cell inside scrollViewWillEndDragging(_:, withVelocity:, targetContentOffset:)
method:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Stop scrollView sliding:
targetContentOffset.pointee = scrollView.contentOffset
let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)
commentsPagingHelper.handleDraggingWillEndForScrollView(scrollView, inside: collectionViewLayout, withVelocity: velocity, usingIndexOfMajorCell: indexOfMajorCell)
}
That's it. Hope, this will help someone with similar issue.
Paging in Collection View not centering properly
You have to make sure that your section inset + line spacing + cell width all equals exactly the bounds of the CollectionView. I use the following method on a custom UICollectionViewFlowLayout sublcass:
- (void)adjustSpacingForBounds:(CGRect)newBounds {
NSInteger count = newBounds.size.width / self.itemSize.width - 1;
CGFloat spacing = (newBounds.size.width - (self.itemSize.width * count)) / count;
self.minimumLineSpacing = spacing;
UIEdgeInsets insets = self.sectionInset;
insets.left = spacing/2.0f;
self.sectionInset = insets;
}
You have to remember that UICollectionView is just a sublcass of UIScrollView so it doesn't know how you want your pagination to work. Before UICollectionView you would have to handle all of this math when laying out your subviews of the UIScrollView.
Paging UICollectionView by cells, not screen
OK, so I found the solution here: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout
I should have searched for targetContentOffsetForProposedContentOffset
in the begining.
Related Topics
Error Appstore Connect:Missing Purpose String in Info.Plist File
How to Debug "Invalid Bundle" Error Which Happens Only After Submitting to App Store
Compare Two Version Strings in Swift
Swiftui: Unwanted Split View on iPad
Wanted: How to Reliably, Consistently Select an Mkmapview Annotation
iOS Changing Uiscrollview Scrollbar Color to Different Colors
Repeat Image Horizontally and Vertically
iOS Unable to Find Plugins, Android Fine
Uiview Animation Jumps at Beginning
The Identity Used to Sign the Executable Is No Longer Valid
Multiline Uilabel with Adjustsfontsizetofitwidth
How to Calculate the Height of an Nsattributedstring with Given Width in iOS 6
Ios: Determine If Device Language Is Right to Left (Rtl)
Uicollectionview Registerclass: Forcellwithreuseidentifier Method Breaks Uicollectionview
How to Set an Title of the Currently Playing Audio in iPhone Lock Screen
Non Power of Two Textures in iOS
Ios: Differencebetween -Init and -Viewload of a Viewcontroller