UICollectionView sticky column glitch with contentOffset - ios

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

Related

UICollectionView find center cell when content inset is used

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;
});
}
}
}

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;
}
}

List like Fleck iOS app

I want to create a scroll view like Fleck app (https://itunes.apple.com/us/app/fleck-the-bigger-picture/id619977675). When you scroll items change his height. One increases the other decreases
I created this. But this work queerly. Perhaps there are similar elements. They could help me...
#interface FleckView()
#property (strong, nonatomic) NSMutableArray* items;
#property (strong, nonatomic) NSMutableArray* cells;
#property (assign, nonatomic) CGFloat old_y;
#property (assign, nonatomic) CGFloat step;
#end
#implementation FleckView
#define kCellHeight 80.0
#define kCellHeightActive 320.0
int counter = 0;
float diff = (kCellHeightActive - kCellHeight) / kCellHeight; // 3.5
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(void) loadItems: (NSMutableArray*) items{
self.old_y = 0;
self.step = 0;
self.delegate = self;
self.cells = [[NSMutableArray alloc] init];
self.items = items;
self.contentSize = CGSizeMake(320, kCellHeightActive + (self.items.count) * kCellHeight);
//add first
UIView *view = [self.items objectAtIndex:0];
view.frame = CGRectMake(0, 0, 320, kCellHeightActive);
[self.cells addObject: view];
[self addSubview:view];
//add other
for(int i = 1; i < self.items.count; i++){
UIView *view = [self.items objectAtIndex:i];
view.frame = CGRectMake(0, ((i - 1) * kCellHeight) + kCellHeightActive, 320, kCellHeight);
[self.cells addObject: view];
[self addSubview:view];
}
}
////MAIN WORK
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
counter = (scrollView.contentOffset.y) / kCellHeight ;
self.step = scrollView.contentOffset.y - self.old_y;
self.old_y = scrollView.contentOffset.y;
if(counter < self.items.count){
[self setHeightRow: counter + 1];
}
}
-(void) setHeightRow: (int) rowNum{
UIView *cell_prev = [self.cells objectAtIndex: rowNum - 1];
UIView *cell = [self.cells objectAtIndex: rowNum];
CGFloat height = cell.frame.size.height;
height = height + (self.step * diff);
if(height < 90)
height = 80;
CGFloat height_prev = kCellHeightActive + kCellHeight - height;
NSLog(#"HEIGHT_PREV: %f", height_prev);
NSLog(#"HEIGHT: %f", height);
CGFloat y_prev = counter * kCellHeight;
CGFloat y = y_prev + height_prev;
NSLog(#"___Y_PREV: %f", y_prev);
NSLog(#"___Y: %f", y);
cell_prev.frame = CGRectMake(0, y_prev, 320, height_prev);
cell.frame = CGRectMake(0, y, 320, height);
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
float bottom = counter * kCellHeight;
float top = (counter + 1) * kCellHeight;
if(targetContentOffset->y > kCellHeight / 2 + counter * kCellHeight){
targetContentOffset->y = top;
}
else{
targetContentOffset->y = bottom;
}
}

Creating a stretchy UICollectionView like Evernote on iOS 7

I've been working on trying to recreate the stretchy collection view that Evernote uses in iOS 7 and I'm really close to having it working. I've managed to create a custom collection view flow layout that modifies the layout attribute transforms when the content offset y value lies outside collection view bounds. I'm modifying the layout attributes in the layoutAttributesForElementsInRect method and it behaves as expected except that the bottom cells can disappear when you hit the bottom of the scroll view. The further you pull the content offset the more cells can disappear. I think the cells basically get clipped off. It doesn't happen at the top though and I'd expect to see the same behavior in both places. Here's what my flow layout implementation looks like right now.
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y;
CGFloat scrollResistance = distanceFromTop / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight - item.center.y;
CGFloat scrollResistance = distanceFromBottom / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Handle cases where the layout needs to be rebuilt
if (yPosition < topOffset)
return YES;
else if (yPosition > bottomOffset)
return YES;
else if (_transformsNeedReset)
return YES;
return NO;
}
#end
I also zipped up the project for people to try out. Any help would be greatly appreciated as I'm pretty new to creating custom collection view layouts. Here's the link to it:
https://dl.dropboxusercontent.com/u/2975688/StackOverflow/stretchy_collection_view.zip
Thanks everyone!
I was able to solve the problem. I'm not sure if there's actually a bug in iOS or not, but the issue was that the cells were actually getting translated outside the content view of the collection view. Once the cell would get translated far enough, it would get clipped off. I find it interesting that this does not happen in the simulator for non-retina displays, but does with retina displays which is why I feel this may actually be a bug.
With that in mind, a workaround for now is to add padding to the top and bottom of the collection view by overriding the collectionViewContentSize method. Once you do this, if you add padding to the top, you need to adjust the layout attributes for the cells as well so they are in the proper location. The final step is to set the contentInset on the collection view itself to adjust for the padding. Leave the scroll indicator insets alone since those are fine. Here's the implementation of my final collection view controller and the custom flow layout.
CNStretchyCollectionViewController.m
#implementation CNStretchyCollectionViewController
static NSString *CellIdentifier = #"Cell";
- (void)viewDidLoad
{
// Register the cell
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
// Tweak out the content insets
CNStretchyCollectionViewFlowLayout *layout = (CNStretchyCollectionViewFlowLayout *) self.collectionViewLayout;
self.collectionView.contentInset = layout.bufferedContentInsets;
// Set the delegate for the collection view
self.collectionView.delegate = self;
self.collectionView.clipsToBounds = NO;
// Customize the appearance of the collection view
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleDefault;
}
#pragma mark - UICollectionViewDataSource Methods
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
if ([indexPath row] % 2 == 0)
cell.backgroundColor = [UIColor orangeColor];
else
cell.backgroundColor = [UIColor blueColor];
return cell;
}
#end
CNStretchyCollectionViewFlowLayout.m
#interface CNStretchyCollectionViewFlowLayout ()
- (CGSize)collectionViewContentSizeWithoutOverflow;
#end
#pragma mark -
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
UIEdgeInsets _contentOverflowPadding;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
_contentOverflowPadding = UIEdgeInsetsMake(100.0f, 0.0f, 100.0f, 0.0f);
_bufferedContentInsets = _contentOverflowPadding;
_bufferedContentInsets.top *= -1;
_bufferedContentInsets.bottom *= -1;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (CGSize)collectionViewContentSize
{
CGSize contentSize = [super collectionViewContentSize];
contentSize.height += _contentOverflowPadding.top + _contentOverflowPadding.bottom;
return contentSize;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation (need to adjust the rect to account for buffer spacing)
rect = UIEdgeInsetsInsetRect(rect, _bufferedContentInsets);
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Shift all the items down due to the content overflow padding
for (UICollectionViewLayoutAttributes *item in items)
{
CGPoint center = item.center;
center.y += _contentOverflowPadding.top;
item.center = center;
}
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = [self collectionViewContentSizeWithoutOverflow].height;
CGFloat topOffset = _contentOverflowPadding.top;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height + _contentOverflowPadding.top;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y - _contentOverflowPadding.top;
CGFloat scrollResistance = distanceFromTop / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight + _contentOverflowPadding.top - item.center.y;
CGFloat scrollResistance = distanceFromBottom / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
#pragma mark - Private Methods
- (CGSize)collectionViewContentSizeWithoutOverflow
{
return [super collectionViewContentSize];
}
#end
CNStretchyCollectionViewFlowLayout.h
#interface CNStretchyCollectionViewFlowLayout : UICollectionViewFlowLayout
#property (assign, nonatomic) UIEdgeInsets bufferedContentInsets;
#end
I'm actually going to through this onto Github and I'll post a link to the project once it's up. Thanks again everyone!

Resources