UICollectionView find center cell when content inset is used - ios

I'm trying to figure out how to find the center most cell of my UICollectionView, which is set to only scroll horizontally.
I tried this SO: how to get indexPath for cell which is located in the center of UICollectionView but it isn't working out, I think because I'm using the contentInset property of my UICollectionView.
What I'm doing is setting the contentInset left and right of the flow layout to exactly 1/2 of self.view's bounds width minus 1/2 of itemSize width. I'm not sure how to calculate the center most cell due to this. I'd appreciate any help offered. Code:
CGPoint cellCenter = CGPointMake((self.collectionView.center.x +
self.collectionView.contentOffset.x) -
(self.sampleFlowLayout.sectionInset.left +
self.sampleFlowLayout.sectionInset.right), self.collectionView.center.y);
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:cellCenter];
if (indexPath)
{
NSInteger tag = [self.collectionView cellForItemAtIndexPath:indexPath].tag;
if (tag != self.currentSample)
{
self.currentSample = tag;
self.imageView.image = [self sampleImageWithOption:self.currentSample];
}
}
- (void)viewDidLoad
{
self.sampleFlowLayout = [[UICollectionViewFlowLayout alloc]init];
CGSize itemSize = CGSizeMake(63, 63);
self.sampleFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sampleFlowLayout.sectionInset = UIEdgeInsetsMake(0, (self.view.bounds.size.width / 2) - (itemSize.width / 2), 0, (self.view.bounds.size.width / 2) - (itemSize.width / 2));
}

I figured this out myself using help from this answer: https://stackoverflow.com/a/22696037/1533438
Final code:
- (void)snapCollectionView:(UICollectionView *)collectionView withProposedOffset:(CGPoint)proposedOffset
{
CGRect cvBounds = collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5;
CGFloat proposedContentOffsetCenterX = proposedOffset.x + halfWidth;
NSArray *attributesArray = [collectionView.collectionViewLayout layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes *candidateAttributes;
for (UICollectionViewLayoutAttributes *attributes in attributesArray)
{
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell)
{
continue;
}
if (!candidateAttributes)
{
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX))
{
candidateAttributes = attributes;
}
}
CGPoint offset = CGPointMake(candidateAttributes.center.x - halfWidth, proposedOffset.y);
[collectionView setContentOffset:offset animated:YES];
CGPoint cellCenter = CGPointMake(offset.x + self.sampleCollectionView.bounds.size.width / 2.0, self.sampleCollectionView.bounds.size.height / 2.0);
NSIndexPath *indexPath = [self.sampleCollectionView indexPathForItemAtPoint:cellCenter];
if (indexPath)
{
NSInteger tag = [self.sampleCollectionView cellForItemAtIndexPath:indexPath].tag;
if (tag != self.currentSample)
{
self.currentSample = tag;
UIImage *image;
if (self.currentSample == 0)
{
image = self.image;
}
else
{
image = [self sampleImageWithOption:self.currentSample];
}
dispatch_async(dispatch_get_main_queue(),
^{
self.imageView.image = image;
});
}
}
}

Related

UICollectionView sticky column glitch with contentOffset

Issue
I've created a UICollectionViewFlowLayout that uses a sticky top row and sticky left column. With this layout i've also added a pinch gesture to the collectionView to enable zooming.
The zooming works perfect when the collectionView isn't scrolled all the way to the right, or the bottom; when this is true the contentOffset.x and contentOffset.y are behaving weirdly.
Can anyone spot the problem? I've been trying to debug this issue for some time now.
Here's a gif representation:
Code
Grid
- (void)actionPinchGesture:(UIPinchGestureRecognizer *)gesture {
MyCustomLayout *layout = (MyCustomLayout *)self.gridView.collectionViewLayout;
if (gesture.state == UIGestureRecognizerStateBegan) {
[layout setPreviousPinchValue:gesture.scale];
}
if (gesture.state == UIGestureRecognizerStateChanged) {
[layout setPinchValue:gesture.scale];
[layout updatePinchValues];
}
}
MyCustomLayout.h
#property (nonatomic, assign) previousPinchValue;
#property (nonatomic, assign) pinchValue;
- (void)updateGridScale;
MyCustomLayout.m
#interface MyCustomLayout() {
CGFloat zoomLevel; // stored zoom level.
CGFloat cachedSectionWidth; // stored original section width.
CGFloat cachedColumnWidth; // stored original column width.
CGFloat cachedRowHeight; // stored row height.
// these values above are cached when the grid is setup for the first time
}
#end
#implementation MyCustomLayout
- (void)updateGridScale {
float scaleDeltaFactor = self.pinchValue / self.previousPinchValue;
float currentZoom = self->zoomLevel;
float newZoom = currentZoom * scaleDeltaFactor;
float kMaxZoom = 2.0;
float kMinZoom = 0.47;
newZoom = MAX(kMinZoom, MIN(newZoom, kMaxZoom));
self->zoomLevel = newZoom;
self.previousPinchValue = self.pinchValue;
CGFloat sectionWidth = self->cachedSectionWidth;
CGFloat columnWidth = self->cachedColumnWidth;
CGFloat rowHeight = self->cachedRowHeight;
sectionWidth = sectionWidth * newZoom;
columnWidth = columnWidth * newZoom;
rowHeight = rowHeight * newZoom;
if (newZoom == 1 || newZoom == 0) {
sectionWidth = self->cachedSectionWidth;
columnWidth = self->cachedColumnWidth;
}
self.topColumnWidth = columnWidth;
self.sectionHeight = rowHeight;
[self invalidateLayoutCache];
[self.collectionView reloadData];
}
- (void)prepareLayout {
[super prepareLayout];
[self prepareAttributesForRows:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.collectionView.numberOfSections)]];
[self.allAttributes addObjectsFromArray:self.sectionAttributes];
}
- (void)prepareAttributesForRows:(NSIndexSet *)sectionIndexes {
NSIndexPath *sectionIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
CGFloat sectionMinY = self.hourHeaderHeight + self.contentMargin.top;
CGFloat sectionMinX = (self.collectionView.contentOffset.x - 0.5) - self.collectionView.contentInset.left;
CGFloat sectionY = sectionMinY + ((self.sectionHeight + self.sectionGap) * section);
CGFloat height = self.sectionHeight + 1;
UICollectionViewLayoutAttributes *atts = [self layoutAttributesForSupplementaryViewAtIndexPath:sectionIndexPath ofKind:INSEPGLayoutElementKindSectionHeader withItemCache:self.sectionHeaderAttributes];
atts.frame = CGRectMake(sectionMinX, sectionY, self.sectionHeaderWidth + 1.5, height);
atts.zIndex = [self zIndexForElementKind:INSEPGLayoutElementKindSectionHeader floating:YES];
NSString *indexPathString = [NSString stringWithFormat:#"%li", sectionIndexPath.section];
NSNumber *y_value = [NSNumber numberWithFloat:round(sectionY)];
if (![[self.cachedSectionInfo allKeys] containsObject:indexPathString]) {
[self.cachedSectionInfo setObject:y_value forKey:indexPathString];
}
}
#end

Cells order in UICollectionView

I need to create a UICollectionView with cells of different sizes (1x1, 2x2, 2x1, 3x2.5). I've already code to add cells depending on which size they are using collectionView:layout:sizeForItemAtIndexPath:.
Here is expected result (cells 1 and 3 have to be on the left of cell 2) :
But current result is :
My (ugly) current code is (self.cellSize = screen width / 3) :
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat size = self.cellSize;
if (indexPath.item == 0) {
return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
}
if (indexPath.item == 1) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 2) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize * 2.f);
}
if (indexPath.item == 3) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 4) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize);
}
if (indexPath.item == 5) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 6) {
return CGSizeMake(self.cellSize, self.cellSize);
}
if (indexPath.item == 7) {
return CGSizeMake(self.cellSize * 2.f, self.cellSize);
}
if (indexPath.item == 8) {
return CGSizeMake(self.cellSize * 3.f, self.cellSize * 2.5f);
}
return CGSizeMake(size, size);
}
Is there a way to specify that cell 3 has to be above cell 1, but on left of cell 2 ?
I don't want to do a static layout that will not change because each cell could be of different size in future. For example, cell 2 could be 2x1 sized ...
Which is the best approach to do that ? I was expecting to specify to UICollectionViewLayout a flow (like "from top"), but it doesn't work like that ...
A sample example by subclassing UICollectionViewLayout.
All values are hard coded, just to explicit the logic behind it. Of course, that could be optimized.
#interface CustomCollectionViewLayout ()
#property (nonatomic, strong) NSMutableDictionary *cellLayouts;
#property (nonatomic, assign) CGSize unitSize;
#end
#implementation CustomCollectionViewLayout
-(id)initWithSize:(CGSize)size
{
self = [super init];
if (self)
{
_unitSize = CGSizeMake(size.width/3,150);
_cellLayouts = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void)prepareLayout
{
for (NSInteger i = 0; i < [[self collectionView] numberOfItemsInSection:0]; i ++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect frame;
switch ([indexPath item])
{
case 0:
frame = CGRectMake(0, 0, _unitSize.width*3, _unitSize.height*2.5);
break;
case 1:
frame = CGRectMake(0, _unitSize.height*2.5, _unitSize.width, _unitSize.height);
break;
case 2:
frame = CGRectMake(_unitSize.width, _unitSize.height*2.5, _unitSize.width*2, _unitSize.height*2);
break;
case 3:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 4:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
break;
case 5:
frame = CGRectMake(_unitSize.width*2, _unitSize.height*2.5+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 6:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width, _unitSize.height);
break;
case 7:
frame = CGRectMake(_unitSize.width, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*2, _unitSize.height);
break;
case 8:
frame = CGRectMake(0, _unitSize.height*2.5+_unitSize.height+_unitSize.height+_unitSize.height+_unitSize.height, _unitSize.width*3, _unitSize.height*2.5);
break;
default:
frame = CGRectZero;
break;
}
[attributes setFrame:frame];
[[self cellLayouts] setObject:attributes forKey:indexPath];
}
}
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *retAttributes = [[NSMutableArray alloc] init];
for (NSIndexPath *anIndexPath in [self cellLayouts])
{
UICollectionViewLayoutAttributes *attributes = [self cellLayouts][anIndexPath];
if (CGRectIntersectsRect(rect, [attributes frame]))
{
[retAttributes addObject:attributes];
}
}
return retAttributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self cellLayouts][indexPath];
}
-(CGSize)collectionViewContentSize
{
return CGSizeMake(_unitSize.width*3, _unitSize.height*9);
}
#end
Then, you just have to call :
CustomCollectionViewLayout *layout = [[CustomCollectionViewLayout alloc] initWithSize:self.myCollectionView.bounds.frame.size];
[self.myCollectionView setCollectionViewLayout:layout];
Rendering :
Here it's the same logic as Larme suggested. But with less hardcode and which give you the possibility of setting the number of items you want without adding new case:.
I call "pattern" a set of 5 items. So first I define constant values:
#define MAX_COLUMN 3 // Max columns in the pattern
#define MAX_LINE_PER_PATTERN 3 // Max lines in the pattern
#define PATTERN_ITEM_COUNT 5 // Max items in the pattern
Then I create a custom layout with 2 properties NSMutableArray *layoutAttributes and CGFloat contentHeight in which I need to override the methods:
- (void)prepareLayout{
[super prepareLayout];
if (!self.layoutAttributes){
self.layoutAttributes = [[NSMutableArray alloc] init];
CGFloat cellWidth = self.collectionView.frame.size.width / MAX_COLUMN;
CGFloat cellHeight = cellWidth;
self.contentHeight = 0.f;
for (int item = 0 ; item < [self.collectionView numberOfItemsInSection:0] ; item ++){
CGFloat width, height = 0.f;
CGFloat xPos, yPos = 0.f;
NSInteger patternCount = (NSInteger)((CGFloat)item / (CGFloat)PATTERN_ITEM_COUNT);
NSInteger currentIndex = item % PATTERN_ITEM_COUNT;
switch (currentIndex) {
case 0:
{
xPos = 0.f;
yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
self.contentHeight += cellHeight;
break;
}
case 1:
{
xPos = cellWidth;
yPos = 0.f + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth * 2.f;
height = cellHeight * 2.f;
self.contentHeight += cellHeight;
break;
}
case 2:
{
xPos = 0.f;
yPos = cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
break;
}
case 3:
{
xPos = 0.f;
yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth * 2.f;
height = cellHeight;
self.contentHeight += cellHeight;
break;
}
case 4:
{
xPos = 2.f * cellWidth;
yPos = 2.f * cellHeight + MAX_LINE_PER_PATTERN * cellHeight * patternCount;
width = cellWidth;
height = cellHeight;
break;
}
default:
NSLog(#"error with index");
break;
}
UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForRow:item inSection:0]];
attr.frame = CGRectMake(xPos,
yPos,
width,
height);
[self.layoutAttributes addObject:attr];
}
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
NSMutableArray *currentAttributes = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *attr in self.layoutAttributes) {
if (CGRectIntersectsRect(attr.frame, rect))
{
[currentAttributes addObject:attr];
}
}
return currentAttributes;
}
- (CGSize)collectionViewContentSize{
return CGSizeMake(self.collectionView.frame.size.width, self.contentHeight);
}
Then assign this custom layout to self.collectionView.collectionViewLayout and that's it. You can find more information and swift version here.

Dynamically resize UICollectionViewCell in non-scrolling UICollectionView

I have a small, single row horizontal layout UICollectionView at the top of the screen. It can contain up to a maximum of 6 items. The problem is that I want all 6 items visible without scrolling (this collection view is also going to be used in a Today extension which doesn't allow scrolling). What I want to do is reduce the cell-size and inter-item spacing a little bit to allow all 6 cells to fit.
Basically I'm trying to avoid this:
I've been playing with this for a while but I'm not sure how to approach it. I created a method that's fired every time an item is added or removed from the collection view, just before [self.collectionview reloadData] is called.
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
CGSize itemsize = CGSizeMake(58,58);
CGFloat itemsizeWidth = itemsize.width;
CGFloat totalWidth = sectionInsetLeft + sectionInsetRight +
(itemsizeWidth * _armedRemindersArray.count) +
(minItemSpacing * (_armedRemindersArray.count -2));
CGFloat reductionAmount = itemsizeWidth;
if (totalWidth > screenwidth) {
while (totalWidth > screenwidth) {
totalWidth = totalWidth - 1;
reductionAmount = reductionAmount - 1;
}
CGSize newCellSize = CGSizeMake(reductionAmount, reductionAmount);
flowLayout.itemSize = newCellSize;
}
else flowLayout.itemSize = itemsize;
}
This is the result.
Not exactly what I was expecting. Not only did it squash everything to the left and also added a second line, but I also seem to have a cell-reuse issue. Truthfully I would just use static-cells if it was even an option, but unfortunately it seems like it's not possible.
What should I be doing? Subclassing UICollectionViewFlowLayout? Won't that basically do the same thing I'm doing here with the built-in flow layout?
EDIT:
Kujey's answer is definitely closer to what I need. I still have a cell-reuse issue though.
Xcode provides an object designed for your need. It's called UICollectionViewFlowLayout and all you need to do is subclass it and place your cells the way you want. The function prepareForLayout is call every time the collection view needs to update the layout.
The piece of code you need is below :
#import "CustomLayout.h"
#define MainCell #"MainCell"
#interface CustomLayout ()
#property (nonatomic, strong) NSMutableDictionary *layoutInfo;
#end
#implementation CustomLayout
-(NSMutableDictionary *) layoutInfo
{
if (!_layoutInfo) {
_layoutInfo = [NSMutableDictionary dictionary];
}
return _layoutInfo;
}
-(void) prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
CGFloat itemWidth;
CGFloat itemSpacing;
CGFloat widthWithoutSpacing = [self collectionViewContentSize].width / ([self.collectionView numberOfItemsInSection:0]);
if (widthWithoutSpacing > [self collectionViewContentSize].height) {
itemWidth = [self collectionViewContentSize].height;
itemSpacing = ([self collectionViewContentSize].width - itemWidth*[self.collectionView numberOfItemsInSection:0])/
([self.collectionView numberOfItemsInSection:0]+1);
}
else {
itemWidth = widthWithoutSpacing;
itemSpacing = 0;
}
CGFloat xPosition = itemSpacing;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
for (NSInteger index = 0 ; index < [self.collectionView numberOfItemsInSection:section] ; index++) {
indexPath = [NSIndexPath indexPathForItem:index inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect currentFrame=itemAttributes.frame;
currentFrame.origin.x = xPosition;
currentFrame.size.width = itemWidth;
currentFrame.size.height = itemWidth;
itemAttributes.frame=currentFrame;
cellLayoutInfo[indexPath] = itemAttributes;
xPosition += itemWidth + itemSpacing;
}
}
self.layoutInfo[MainCell] = cellLayoutInfo;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier, NSDictionary *elementsInfo, BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *innerStop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
}];
return allAttributes;
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[MainCell][indexPath];
}
-(CGSize) collectionViewContentSize
{
return self.collectionView.frame.size;
}
#end
You can also change the y origin of your cells if you need to center them vertically.
try with this code. I get the width and use _armedRemindersArray (i guess you use this array for the items).
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
//Figure out if cells are wider than screen
CGFloat screenwidth = self.view.frame.size.width;
CGFloat width = screenwidth - ((sectionInsetLeft + sectionInsetRight) *_armedRemindersArray.count + minItemSpacing * (_armedRemindersArray.count -2));
CGSize itemsize = CGSizeMake(width,width);
flowLayout.itemSize = itemsize;
}
I don't know why you're setting the itemsize first, and then reducing it. I think you should do it the other way around:
-(void)setupCollectionViewLayout{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout*)self.buttonBarCollectionView.collectionViewLayout;
CGFloat sectionInsetLeft = 10;
CGFloat sectionInsetRight = 10;
CGFloat minItemSpacing = flowLayout.minimumInteritemSpacing;
// Find the appropriate item width (<= 58)
CGFloat screenwidth = self.view.frame.size.width;
CGFloat itemsizeWidth = (screenwidth - sectionInsetLeft - sectionInsetRight - (minItemSpacing * (_armedRemindersArray.count -2))) / _armedRemindersArray.count
itemsizeWidth = itemsizeWidth > 58 ? 58 : itemsizeWidth;
flowLayout.itemSize = CGSizeMake(itemsizeWidth, itemsizeWidth);
}
Does this work? If not, could you please include more of your code?

UICollectionViewCell disappears in iOS 8

I have a collectionView which works well in iOS 7 and now in iOS 8 is acts strangely.
when collectionView appears it only displays one cell: (it must be 2)
but after scrolling a bit the second cell appears
Im using a custom collectionViewFlowLayout. but changing to UICollectionViewFlowLayout doesn't fix the issue.
Cell Size : 657, 500
Min Spacing For Lines : 100
Min Spacing For Cells : 10
I have added left and right edge insets: (if I remove the insets it works well. but I must use insets to keep my cell at the center of view)
- (UIEdgeInsets)collectionView:(UICollectionView *)cv
layout:(UICollectionViewLayout *)cvl
insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, (cv.bounds.size.width - 657) / 2.0f, 0,
(cv.bounds.size.width - 657) / 2.0f);
}
Here is my custom flow layout:
#import "CoverFlowLayout.h"
static const CGFloat kMaxDistancePercentage = 0.3f;
//static const CGFloat kMaxRotation = (CGFloat)(50.0 * (M_PI / 180.0));
static const CGFloat kMaxZoom = 0.1f;
#implementation CoverFlowLayout
- (id)init {
if ((self = [super init])) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumLineSpacing = 10000.0f; }
return self;
}
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
// 1
CGRect visibleRect =
(CGRect){.origin = self.collectionView.contentOffset,
.size = self.collectionView.bounds.size};
CGFloat maxDistance =
visibleRect.size.width * kMaxDistancePercentage;
// 2
NSArray *array = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attributes in array) {
// 3
CGFloat distance =
CGRectGetMidX(visibleRect) - attributes.center.x;
// 4
CGFloat normalizedDistance = distance / maxDistance;
normalizedDistance = MIN(normalizedDistance, 1.0f);
normalizedDistance = MAX(normalizedDistance, -1.0f);
// 5
//CGFloat rotation = normalizedDistance * kMaxRotation;
CGFloat zoom = 1.0f + ((1.0f - ABS(normalizedDistance)) * kMaxZoom);
// 6
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -1000.0;
//transform = CATransform3DRotate(transform,
// rotation, 0.0f, 1.0f, 0.0f);
transform = CATransform3DScale(transform, zoom, zoom, zoom);
attributes.transform3D = transform;
}
// 7
return array;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
- (CGPoint)targetContentOffsetForProposedContentOffset: (CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
// 1
CGFloat offsetAdjustment = CGFLOAT_MAX;
CGFloat horizontalCenter = proposedContentOffset.x +
(CGRectGetWidth(self.collectionView.bounds) / 2.0f);
// 2
CGRect targetRect = CGRectMake(proposedContentOffset.x,
0.0f, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array =
[super layoutAttributesForElementsInRect:targetRect];
for (UICollectionViewLayoutAttributes* layoutAttributes in array)
{
// 3
CGFloat distanceFromCenter = layoutAttributes.center.x - horizontalCenter;
if (ABS(distanceFromCenter) < ABS(offsetAdjustment))
{
offsetAdjustment = distanceFromCenter;
}
}
// 4
return CGPointMake(proposedContentOffset.x + offsetAdjustment,
proposedContentOffset.y);
}
initially in overrided layoutAttributesForElementsInRect visible rect is {0,0, 1024, 768}
but [super layoutAttributesForElementsInRect:rect]; returns only one UICollectionViewCellAttribute. (it should be 2)
is any idea how can I fix this?
I don know How it can be cause of the issue but it was originated from:
NSIndexPath *visibleIndexPath = [self.collectionView indexPathForItemAtPoint:midPoint];
I want to update my pageControl to indicate which cell is at the center of screen.
I Changed my method and now it works well:
//*****updating page control*****
// get the visible rect
CGRect visibleRect = (CGRect) {.origin = self.collectionView.contentOffset,
.size = self.collectionView.bounds.size};
// get the mid point in the visible rect
CGPoint midPoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
// find indexPath of the item in that midPoint
//in iOS 8 Cause the second cell disappear
//NSIndexPath *visibleIndexPath = [self.collectionView indexPathForItemAtPoint:midPoint];
//iterating through visble cells to find the cell which contains midpoint then get get that cell indexpath
for (UICollectionViewCell *cell in [self.collectionView visibleCells]) {
if (CGRectContainsPoint(cell.frame, midPoint)) {
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
//update page control
self.pageControl.currentPage = indexPath.row;
//quiting loop
break;
}
}

How to fix scrolling to a visible cell in UICollectionView with custom layout?

I'm working on custom value picker inspired by UIPickerView. It looks like that:
As you can see, one of the main features of this picker is the central cell the should be wider than others to make it's neighbours visible beside of the central frame. When you scroll the picker with a pan gesture it should dynamically change central value and adjust cells according to the logic above. And it works just perfect:
The problem is in the tap gesture. When the user select any item on the picker by performing tap on it the picker trying to scroll to that item. But since it's offset was changed by the custom layout UIScrollView scrolls to the wrong point. And it looks like that:
When I trying to scroll to the offscreen cell all things works fine - that cell was not affected by the layout and it's coordinates are correct. The issue rises only for visible cells. I'm completely out of any ideas of how to fix that. You could find the whole project here: Carousel Collection View Test Project
Please find some significant code below.
// CarouselLayout.m
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
CGRect center = CGRectMake(CGRectGetMidX(visibleRect) - 1.0, 0.0, 2.0, CGRectGetHeight(visibleRect));
CGFloat coreWidth = CGRectGetWidth(self.centralFrame) / 3.0;
for (UICollectionViewLayoutAttributes *attributes in array) {
if (CGRectIntersectsRect(attributes.frame, rect)){
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat offset = 0.0;
CGRect coreFrame = CGRectMake(attributes.center.x - (coreWidth / 2.0), 0.0, coreWidth, CGRectGetHeight(self.centralFrame));
if (CGRectIntersectsRect(center, coreFrame)) {
if (attributes.indexPath.item % 2 == 0) {
self.centralItemOffset = (CGRectGetWidth(self.centralFrame) - CGRectGetWidth(attributes.frame) - 4.0) / 2.0;
if ([self.collectionView.delegate respondsToSelector:#selector(collectionView:layout:didChangeCentralItem:)]) {
[(id <CarouselLayoutDelegate>)self.collectionView.delegate collectionView:self.collectionView layout:self didChangeCentralItem:attributes.indexPath];
}
}
}
offset = (distance > 0) ? -self.centralItemOffset : self.centralItemOffset;
attributes.center = CGPointMake(attributes.center.x + offset, attributes.center.y);
}
}
return array;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
CGRect targetRectHorizontal = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRectHorizontal];
for (UICollectionViewLayoutAttributes *attributes in array) {
if (attributes.indexPath.item % 2 == 1) {
continue;
}
CGFloat itemHorizontalCenter = attributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
// ViewController.m
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item % 2 == 1) {
return;
}
NSString *nextValue = [self valueAtIndexPath:indexPath];
[self scrollToValue:nextValue animated:YES];
self.currentValue = nextValue;
}
- (void)scrollToValue:(NSString *)value animated:(BOOL)animated {
NSIndexPath *targetPath = nil;
NSIndexPath *currentPath = nil;
for (NSString *item in self.itemsArray) {
if (!targetPath && [value isEqualToString:item]) {
targetPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
}
if (!currentPath && [self.currentValue isEqualToString:item]) {
currentPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
}
if (targetPath && currentPath) {
break;
}
}
if (targetPath && currentPath) {
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
}
If it's not enough please ask for additional code in comments.
In scrollToValue method change:
if (targetPath && currentPath) {
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
into:
if (targetPath && currentPath) {
if (targetPath.row < currentPath.row) {
[self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.itemsArray.count inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
} else {
[self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
}
and it'll work.
I think you need to manually set the content offset in your scrollToValue method:
[self.itemsCollectionView setContentOffset:offset animated:YES];
To find this offset you need to catch the current cell index (currentCell) and to find the width of cell before. For example:
CGFloat xOffset = 0;
for (int i = 0 ; i < currentCell ; i++)
{
if(i % 2 == 1){
xOffset += dotCellWidth; // 20 in your case
}
else{
// here you need to find the width of the cell at index i (25 or 34 in your case)
xOffset += numberCellWidth
}
}
// finish by adjusting the offset to the center of the current cell
xOffset += currentCellWidth / 2; // 12.5f or 17.f
CGPoint offset = CGPointMake(xOffset, 0.0);
[self.itemsCollectionView setContentOffset:offset animated:YES];

Resources