UICollectionView horizontal paging - can I use Flow Layout?
You're right – that's not how a stock horizontally-scrolling collection view lays out cells. I'm afraid that you're going to have to implement your own custom UICollectionViewLayout
subclass. Either that, or separate your models into sections.
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
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 scroll, horizontal layout?
1st approach
What about using UIPageViewController
with an array of UICollectionViewControllers
? You'd have to fetch proper number of items in each UICollectionViewController
, but it shouldn't be hard. You'd get exactly the same look as the Springboard has.
2nd approach
I've thought about this and in my opinion you have to set:
self.collectionView.pagingEnabled = YES;
and create your own collection view layout by subclassing UICollectionViewLayout
. From the custom layout object you can access self.collectionView
, so you'll know what is the size of the collection view's frame
, numberOfSections
and numberOfItemsInSection:
. With that information you can calculate cells' frames
(in prepareLayout
) and collectionViewContentSize
. Here're some articles about creating custom layouts:
- https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
- http://www.objc.io/issue-3/collection-view-layouts.html
3rd approach
You can do this (or an approximation of it) without creating the custom layout. Add UIScrollView
in the blank view, set paging enabled in it. In the scroll view add the a collection view. Then add to it a width constraint, check in code how many items you have and set its constant
to the correct value, e.g. (self.view.frame.size.width * numOfScreens)
. Here's how it looks (numbers on cells show the indexPath.row
): https://www.dropbox.com/s/ss4jdbvr511azxz/collection_view.mov If you're not satisfied with the way cells are ordered, then I'm afraid you'd have to go with 1. or 2.
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.
Related Topics
Uitableview:Viewforheaderinsection: Not Called During Reloaddata:
Corenfc Not Reading Uid in iOS
How to Count Rows in a Parse Class Programmatically in an iOS App Using Swift
How to Use Multi-Path Update with the Rest API in Firebase? Error 400
iOS Facebook Sdk 4.0 Login Error Code 304
Cellforitematindexpath Not Called But Numberofitemsinsection Does
Storyboard Uiviewcontroller, 'Custom Class' Not Showing in Drop Down
Simplified Screen Capture: Record Video of Only What Appears Within the Layers of a Uiview
Cast Value of Type 'Uitableviewcell' to Custom Cell
Cannot Assign Value of Type '() -> Void' to Type '(() -> Void)!'
Swift: Get All Subviews of a Specific Type and Add to an Array
Avaudiosession Setcategory Swift 4.2 iOS 12 - Play Sound on Silent
Can't Set Titleview in the Center of Navigation Bar Because Back Button
Firebase Querying - I Get Some Type of Nsarray as Result (Only Sometimes)
What Privacy-Violating or Device-Changing Things How to Do on an Iphone