The best way to demonstrate my problem is with the video below:
http://foffer.dk/collectionview.mp4
As you can see, my collectionView doesn't update it's layout to correctly display the items until I start scrolling. I'm using a separate collectionViewFlowLayout model. Here is my code:
CollectionViewFlowLayout.m:
#implementation CollectionViewFlowLayout
- (void)prepareLayout {
CGFloat halfWidth = self.collectionView.bounds.size.width / 2;
CGFloat halfHeight = self.collectionView.bounds.size.height / 2;
self.itemSize = CGSizeMake(halfWidth, halfHeight);
self.minimumInteritemSpacing = 0;
self.minimumLineSpacing = 0;
}
// indicate that we want to redraw as we scroll
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
#end
my PhotosCollectionViewController (I've left out the standart collectionView stuff as I didn't think it is important to this issue, let me know if it is and I will post it):
-(instancetype)init {
UICollectionViewFlowLayout *layout = [[CollectionViewFlowLayout alloc] init];
return (self = [super initWithCollectionViewLayout:layout]);
}
-(void) viewDidLoad{
[super viewDidLoad];
[self.collectionView registerClass:[FOFPhotoCell class] forCellWithReuseIdentifier:#"photo"];
self.collectionView.backgroundColor = [UIColor whiteColor];
CGFloat bottomLayoutGuide = self.tabBarController.tabBar.frame.size.height;
self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, bottomLayoutGuide, 0);
}
-(BOOL)shouldAutorotate{
return YES;
}
Do I need to call invalidateLayout somewhere? I can't seem to figure out where to implement that method. [self.collectionView invalidateLayout] doesn't work.
Could someone get me on the right track here?
Thanks in advance
Chris
So, it turns out I had it set up all wrong. As this article from apple says:
Before you start building custom layouts, consider whether doing so is
really necessary.
The UICollectionViewFlowLayout class provides a
significant amount of behavior that has already been optimized for
efficiency and that can be adapted in several ways to achieve many
different types of standard layouts. The only times to consider
implementing a custom layout are in the following situations:
The layout you want looks nothing like a grid or a line-based breaking
layout (a layout in which items are placed into a row until itโs full,
then continue on to the next line until all items are placed) or
necessitates scrolling in more than one direction.
You want to change all of the cell positions frequently enough that it would be more work
to modify the existing flow layout than to create a custom layout.
And my layout config is really simple. So I was overcomplicating it.
Related
I'm building an app whereas I've got a UICollectionView with a custom layout. I have a problem whereas I cannot tap some rows. Here's an overview over what happens
1) App starts and presents a UICollectionView
2) 3 test items are displayed, using the visual debugger in Xcode, you can see that there is something not right with the hierarchy
The red row can't be tapped now, but the yellow and green rows can.
3) User taps the yellow item, and segues to another page
4) User pops the shown UIViewController and returns to the UICollectionView whereas the viewWillAppear method reloads the UICollectionView like so:
[self.collectionView reloadData];
5) Re-entering the visual debugger shows that the hierarchy seems shifted, and the yellow row is now untappable, but the red and green rows can be tapped.
What could be the reason for this? I'll post any relevant code, ask for what parts you'd like to see.
Update
The UIViewController displaying the UICollectionView
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ShareViewCell *view = [collectionView dequeueReusableCellWithReuseIdentifier:#"share" forIndexPath:indexPath];
view.share = [self.sharesController shareAtIndex:indexPath.item];
return view;
}
Custom cell:
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.leftBorderView = [UIView new];
[self.contentView addSubview:self.leftBorderView];
self.label = [UILabel new];
self.label.font = [UIFont boldSystemFontOfSize:10];
self.label.numberOfLines = 0;
self.label.lineBreakMode = NSLineBreakByWordWrapping;
[self.contentView addSubview:self.label];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.leftBorderView.frame = CGRectMake(0, 0, 1.5, self.bounds.size.height);
CGSize labelSize = [self.label sizeThatFits:CGSizeMake(self.bounds.size.width - 10.0f, MAXFLOAT)];
NSTimeInterval durationOfShare = [self.share.endSpan timeIntervalSinceDate:self.share.startSpan] / 3600;
CGFloat middleOfCell = self.bounds.size.height / 2;
CGFloat yPositionToUse = middleOfCell - (labelSize.height / 2);
if (durationOfShare < 0.25)
{
[self.label setHidden:YES];// to small to be shown
} else if (durationOfShare > 0.5)
{
yPositionToUse = 8; // show it at the top
}
self.label.frame = CGRectMake(8, yPositionToUse, labelSize.width, labelSize.height);
}
Update 2
Is the UICollectionReusableView blocking tap of the UICollectionViewCell?
Ok,
First, you should be more precise in your question : you're using some code found on some question on StackOverflow.
The code seems to be this Calendar UI made with UICollectionView.
This is a sample code, quickly built for the sake of a StackOverflow answer (with a bounty). It's not finished, has no contributors, and you should try to read it and improve over it !
From your debugger's captures, it seems you have some view which overlays on top of your collectionView's cells.
From reading this code, I see it uses some supplementary view to present 'hour' blocks. This supplementary view class is HourReusableView. And it's the view coming on top on your debugger's captures
CalendarViewLayout is responsible to compute these supplementary views frame, as it does for colored event blocks (see method - (void)prepareLayout)
I might bet that these supplementary views's Z-order isn't predictable - all views have a default zIndex of 0. One way to fix it ? After line 67 of this file, set a negative zIndex on the UICollectionViewLayoutAttributes of the hour block. This way, you make sure hour supplementary views are always behind your cells, and don't intercept the cell's touch
I'm developing an app that has a UICollectionView - the collection view's job is to display data from a web service.
One feature of the app I am trying to implement is enabling the user to change the layout of this UICollectionView from a grid view to a table view.
I spent a lot of time trying to perfect this and I managed to get it to work. However there are some issues. The transition between the two layout doesn't look good and sometimes it breaks between switching views and my app is left with a view in an unexpected state. That only happens if the user switches between grid and table view very quickly (pressing the changeLayoutButton) continuously.
So, obviously there are some problems and I feel the code is a little fragile. I also need to fix the above mentioned issues.
I'll start off with how I implemented this view.
Implementation
Since I needed the two different cells (grideCell and tableViewCell) to show different things - I decided it would be better to subclass UICollectionViewFlowLayout since it does everything I need - all I need to do is change the cell sizes.
With that in mind I created two classes that subclassed UICollectionViewFlowLayout
This is how those two classes look:
BBTradeFeedTableViewLayout.m
#import "BBTradeFeedTableViewLayout.h"
#implementation BBTradeFeedTableViewLayout
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(320, 80);
self.minimumLineSpacing = 0.1f;
}
return self;
}
#end
BBTradeFeedGridViewLayout.m
#import "BBTradeFeedGridViewLayout.h"
#implementation BBTradeFeedGridViewLayout
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(159, 200);
self.minimumInteritemSpacing = 2;
self.minimumLineSpacing = 3;
}
return self;
}
#end
Very simple and as you can see - just changing the cell sizes.
Then in my viewControllerA class I implemented the UICollectionView like so:
Created the properties:
#property (strong, nonatomic) BBTradeFeedTableViewLayout *tableViewLayout;
#property (strong, nonatomic) BBTradeFeedGridViewLayout *grideLayout;
in viewDidLoad
/* Register the cells that need to be loaded for the layouts used */
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"BBItemTableViewCell" bundle:nil] forCellWithReuseIdentifier:#"TableItemCell"];
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"BBItemGridViewCell" bundle:nil] forCellWithReuseIdentifier:#"GridItemCell"];
The user taps a button to change between layouts:
-(void)changeViewLayoutButtonPressed
I use a BOOL to determine which layout is currently active and based on that I make the switch with this code:
[self.collectionView performBatchUpdates:^{
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView setCollectionViewLayout:self.grideLayout animated:YES];
} completion:^(BOOL finished) {
}];
In cellForItemAtIndexPath
I determine which cells I should use (grid or tableView) and the load the data - that code looks like this:
if (self.gridLayoutActive == NO){
self.switchToTableLayout = NO;
BBItemTableViewCell *tableItemCell = [collectionView dequeueReusableCellWithReuseIdentifier:tableCellIdentifier forIndexPath:indexPath];
if ([self.searchArray count] > 0){
self.switchToTableLayout = NO;
tableItemCell.gridView = NO;
tableItemCell.backgroundColor = [UIColor whiteColor];
tableItemCell.item = self.searchArray[indexPath.row];
}
return tableItemCell;
}else
{
BBItemTableViewCell *gridItemCell= [collectionView dequeueReusableCellWithReuseIdentifier:gridCellIdentifier forIndexPath:indexPath];
if ([self.searchArray count] > 0){
self.switchToTableLayout = YES;
gridItemCell.gridView = YES;
gridItemCell.backgroundColor = [UIColor whiteColor];
gridItemCell.item = self.searchArray[indexPath.row];
}
return gridItemCell;
}
Lastly in the two cell classes - I just use the data to set the image / text as I need.
Also in grid cell - the image is bigger and I remove text I don't want - which was the primary reason for uses two cells.
I'd be interested in how to make this view look a little more fluid and less buggy in the UI. The look I am going for is just like eBays iOS app - they switch between three different views. I just need to switch between two different views.
#jrturton's answer is helpful, however unless I'm missing something it is really overcomplicating something very simple. I'll start with the points we agree on...
Prevent interaction while changing layouts
First off, I agree with the approach of disabling user interaction at the start of the layout transition & reenabling at the end (in the completion block) using [[UIApplication sharedApplication] begin/endIgnoringInteractionEvents] - this is much better than trying cancel an in-progress transition animation & immediately begin the reverse transition from the current state.
Simplify the layout transition by using a single cell class
Also, I very much agree with the suggestion to use the same cell class for each layout. Register a single cell class in viewDidLoad, and simplify your collectionView:cellForItemAtIndexPath: method to just dequeue a cell and set its data:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
BBItemCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
if ([self.searchArray count] > 0) {
cell.backgroundColor = [UIColor whiteColor];
cell.item = self.searchArray[indexPath.row];
}
return cell;
}
(Notice that the cell itself shouldn't (in all but exceptional cases) need to be aware of anything to do with what layout is currently in use, whether layouts are transitioning, or what the current transition progress is)
Then when you call setCollectionViewLayout:animated:completion: the collection view doesn't need to reload any new cells, it just sets up an animation block to change each cell's layout attributes (you don't need to call this method from inside an performBatchUpdates: block, nor do you need to invalidate the layout manually).
Animating the cell subviews
However as pointed out, you will notice that subviews of the cell jump immediately to their new layout's frames. The solution is to simply force immediate layout of the cells subviews when the layout attributes are updated:
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
[self layoutIfNeeded];
}
(No need to create special layout attributes just for the transition)
Why does this work? When the collection view changes layouts, applyLayoutAttributes: is called for each cell as the collection view is setting up the animation block for that transition. But the layout of the cell's subviews is not done immediately - it is deferred to a later run loop - resulting in the actual subview layout changes not being incorporated into the animation block, so the subviews jump to their final positions immediately. Calling layoutIfNeeded means that we are telling the cell that we want the subview layout to happen immediately, so the layout is done within the animation block, and the subviews' frames are animated along with the cell itself.
It is true that using the standard setCollectionViewLayout:... API does restrict control of the animation timing. If you want to apply a custom easing animation curve then solutions like TLLayoutTransitioning demonstrate a handy way of taking advantage of interactive UICollectionViewTransitionLayout objects to take control of the animation timing. However, as long as only a linear animation of subviews is required I think most people will be satisfied with the default animation, especially given the one-line simplicity of implementing it.
For the record, I'm not keen on the lack of control of this animation myself, so implemented something similar to TLLayoutTransitioning. If this applies to you too, then please ignore my harsh reproval of #jrturton's otherwise great answer, and look into TLLayoutTransitioning or UICollectionViewTransitionLayouts implemented with timers :)
Grid / table transitions aren't as easy as a trivial demo would have you believe. They work fine when you've got a single label in the middle of the cell and a solid background, but once you have any real content in there, it falls over. This is why:
You have no control over the timing and nature of the animation.
While the frames of the cells in the layout are animated from one value to the next, the cells themselves (particularly if you are using two separate cells) don't seem to perform internal layout for each step of the animation so it seems to "flick" from one layout to the next inside each cell - your grid cell looks wrong in table size, or vice versa.
There are many different solutions. It's hard to recommend anything specific without seeing your cell's contents, but I've had success with the following:
take control of the animation using techniques like those shown here. You could also check out Facebook Pop to get better control over the transition but I haven't looked into that in any detail.
use the same cell for both layouts. Within layoutSubviews, calculate a transition distance from one layout to the other and use this to fade out or in unused elements, and to calculate nice transitional frames for your other elements. This prevents a jarring switch from one cell class to the other.
That's the approach I used here to fairly good effect.
It's harder work that relying on resizing masks or Autolayout but it's the extra work that makes things look good.
As for the issue when the user can toggle between the layouts too quickly - just disable the button when the transition starts, and re- enable it when you're done.
As a more practical example, here's a sample of the layout change (some of it is omitted) from the app linked above. Note that interaction is disabled while the transition occurs, I am using the transition layout from the project linked above, and there is a completion handler:
-(void)toggleLayout:(UIButton*)sender
{
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
HMNNewsLayout newLayoutType = self.layoutType == HMNNewsLayoutTable ? HMNNewsLayoutGrid : HMNNewsLayoutTable;
UICollectionViewLayout *newLayout = [HMNNewsCollectionViewController collectionViewLayoutForType:newLayoutType];
HMNTransitionLayout *transitionLayout = (HMNTransitionLayout *)[self.collectionView transitionToCollectionViewLayout:newLayout duration:0.5 easing:QuarticEaseInOut completion:^(BOOL completed, BOOL finish)
{
[[NSUserDefaults standardUserDefaults] setInteger:newLayoutType forKey:HMNNewsLayoutTypeKey];
self.layoutType = newLayoutType;
sender.selected = !sender.selected;
for (HMNNewsCell *cell in self.collectionView.visibleCells)
{
cell.layoutType = newLayoutType;
}
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
}];
[transitionLayout setUpdateLayoutAttributes:^UICollectionViewLayoutAttributes *(UICollectionViewLayoutAttributes *layoutAttributes, UICollectionViewLayoutAttributes *fromAttributes, UICollectionViewLayoutAttributes *toAttributes, CGFloat progress)
{
HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
attributes.progress = progress;
attributes.destinationLayoutType = newLayoutType;
return attributes;
}];
}
Inside the cell, which is the same cell for either layout, I have an image view and a label container. The label container holds all the labels and lays them out internally using auto layout. There are constant frame variables for the image view and the label container in each layout.
The layout attributes from the transition layout are a custom subclass which include a transition progress property, set in the update layout attributes block above. This is passed into the cell using the applyLayoutAttributes method (some other code omitted):
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
self.transitionProgress = 0;
if ([layoutAttributes isKindOfClass:[HMNTransitionLayoutAttributes class]])
{
HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
self.transitionProgress = attributes.progress;
}
[super applyLayoutAttributes:layoutAttributes];
}
layoutSubviews in the cell subclass does the hard work of interpolating between the two frames for the images and labels, if a transition is in progress:
-(void)layoutSubviews
{
[super layoutSubviews];
if (!self.transitionProgress)
{
switch (self.layoutType)
{
case HMNNewsLayoutTable:
self.imageView.frame = imageViewTableFrame;
self.labelContainer.frame = labelContainerTableFrame;
break;
case HMNNewsLayoutGrid:
self.imageView.frame = imageViewGridFrame;
self.labelContainer.frame = self.originalGridLabelFrame;
break;
}
}
else
{
CGRect fromImageFrame,toImageFrame,fromLabelFrame,toLabelFrame;
if (self.layoutType == HMNNewsLayoutTable)
{
fromImageFrame = imageViewTableFrame;
toImageFrame = imageViewGridFrame;
fromLabelFrame = labelContainerTableFrame;
toLabelFrame = self.originalGridLabelFrame;
}
else
{
fromImageFrame = imageViewGridFrame;
toImageFrame = imageViewTableFrame;
fromLabelFrame = self.originalGridLabelFrame;
toLabelFrame = labelContainerTableFrame;
}
CGFloat from = 1.0 - self.transitionProgress;
CGFloat to = self.transitionProgress;
self.imageView.frame = (CGRect)
{
.origin.x = from * fromImageFrame.origin.x + to * toImageFrame.origin.x,
.origin.y = from * fromImageFrame.origin.y + to * toImageFrame.origin.y,
.size.width = from * fromImageFrame.size.width + to * toImageFrame.size.width,
.size.height = from * fromImageFrame.size.height + to * toImageFrame.size.height
};
self.labelContainer.frame = (CGRect)
{
.origin.x = from * fromLabelFrame.origin.x + to * toLabelFrame.origin.x,
.origin.y = from * fromLabelFrame.origin.y + to * toLabelFrame.origin.y,
.size.width = from * fromLabelFrame.size.width + to * toLabelFrame.size.width,
.size.height = from * fromLabelFrame.size.height + to * toLabelFrame.size.height
};
}
self.headlineLabel.preferredMaxLayoutWidth = self.labelContainer.frame.size.width;
}
And that's about it. Basically you need a way of telling the cell how far through the transition it is, which you need the layout transitioning library (or, as I say, Facebook pop might do this) for, and then you need to make sure you get nice values for layout when transitioning between the two.
I'm fairly new to UICollectionView. Though I've watched the WWDC talks on it I'm still unclear how to achieve my layout. I'm trying to constrain my flow layout to a square bottom aligned to the window's rootviewcontroller (see image). However, when setting UIEdgeInsetsMake(200, 10, 10, 10) which as I understand should compress only the top portion of the flow layout, instead what happens is the flow layout is compressed from top and bottom. Additionally, I'm not sure how to use -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect or -(CGSize)collectionViewContentSize to create this layout.
-(id)init
{
self = [super init];
if (self) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(75, 75);
self.sectionInset = UIEdgeInsetsMake(100, 10, 10, 10);
}
return self;
}
What my results are:
What I'm trying to acheive:
I think the frame of collection view is different from what you need .Set it properly
I have a UICollectionView with a FLowLayout. It will work as I expect most of the time, but every now and then one of the cells does not wrap properly. For example, the the cell that should be on in the first "column" of the third row if actually trailing in the second row and there is just an empty space where it should be (see diagram below). All you can see of this rouge cell is the left hand side (the rest is cut off) and the place it should be is empty.
This does not happen consistently; it is not always the same row. Once it has happened, I can scroll up and then back and the cell will have fixed itself. Or, when I press the cell (which takes me to the next view via a push) and then pop back, I will see the cell in the incorrect position and then it will jump to the correct position.
The scroll speed seems to make it easier to reproduce the problem. When I scroll slowly, I can still see the cell in the wrong position every now and then, but then it will jump to the correct position straight away.
The problem started when I added the sections insets. Previously, I had the cells almost flush against the collection bounds (little, or no insets) and I did not notice the problem. But this meant the right and left of the collection view was empty. Ie, could not scroll. Also, the scroll bar was not flush to the right.
I can make the problem happen on both Simulator and on an iPad 3.
I guess the problem is happening because of the left and right section insets... But if the value is wrong, then I would expect the behavior to be consistent. I wonder if this might be a bug with Apple? Or perhaps this is due to a build up of the insets or something similar.
Follow up: I have been using this answer below by Nick for over 2 years now without a problem (in case people are wondering if there are any holes in that answer - I have not found any yet). Well done Nick.
There is a bug in UICollectionViewFlowLayout's implementation of layoutAttributesForElementsInRect that causes it to return TWO attribute objects for a single cell in certain cases involving section insets. One of the returned attribute objects is invalid (outside the bounds of the collection view) and the other is valid. Below is a subclass of UICollectionViewFlowLayout that fixes the problem by excluding cells outside of the collection view's bounds.
// NDCollectionViewFlowLayout.h
#interface NDCollectionViewFlowLayout : UICollectionViewFlowLayout
#end
// NDCollectionViewFlowLayout.m
#import "NDCollectionViewFlowLayout.h"
#implementation NDCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *newAttributes = [NSMutableArray arrayWithCapacity:attributes.count];
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if ((attribute.frame.origin.x + attribute.frame.size.width <= self.collectionViewContentSize.width) &&
(attribute.frame.origin.y + attribute.frame.size.height <= self.collectionViewContentSize.height)) {
[newAttributes addObject:attribute];
}
}
return newAttributes;
}
#end
See this.
Other answers suggest returning YES from shouldInvalidateLayoutForBoundsChange, but this causes unnecessary recomputations and doesn't even completely solve the problem.
My solution completely solves the bug and shouldn't cause any problems when Apple fixes the root cause.
Put this into the viewController that owns the collection view
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.collectionView.collectionViewLayout invalidateLayout];
}
i discovered similar problems in my iPhone application. Searching the Apple dev forum brought me this suitable solution which worked in my case and will probably in your case too:
Subclass UICollectionViewFlowLayout and override shouldInvalidateLayoutForBoundsChange to return YES.
//.h
#interface MainLayout : UICollectionViewFlowLayout
#end
and
//.m
#import "MainLayout.h"
#implementation MainLayout
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return YES;
}
#end
A Swift version of Nick Snyder's answer:
class NDCollectionViewFlowLayout : UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
let contentSize = collectionViewContentSize
return attributes?.filter { $0.frame.maxX <= contentSize.width && $0.frame.maxY < contentSize.height }
}
}
I've had this problem as well for a basic gridview layout with insets for margins. The limited debugging I've done for now is implementing - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect in my UICollectionViewFlowLayout subclass and by logging what the super class implementation returns, which clearly shows the problem.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attrsList = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *attrs in attrsList) {
NSLog(#"%f %f", attrs.frame.origin.x, attrs.frame.origin.y);
}
return attrsList;
}
By implementing - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath I can also see that it seems to return the wrong values for itemIndexPath.item == 30, which is factor 10 of my gridview's number of cells per line, not sure if that's relevant.
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes *attrs = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
NSLog(#"initialAttrs: %f %f atIndexPath: %d", attrs.frame.origin.x, attrs.frame.origin.y, itemIndexPath.item);
return attrs;
}
With a lack of time for more debugging, the workaround I've done for now is reduced my collectionviews width with an amount equal to the left and right margin. I have a header that still needs the full width so I've set clipsToBounds = NO on my collectionview and then also removed the left and right insets on it, seems to work. For the header view to then stay in place you need to implement frame shifting and sizing in the layout methods that are tasked with returning layoutAttributes for the header view.
I have added a bug report to Apple. What works for me is to set bottom sectionInset to a value less than top inset.
I was experiencing the same cell-deplacing-problem on the iPhone using a UICollectionViewFlowLayout and so I was glad finding your post. I know you are having the problem on an iPad, but I am posting this because I think it is a general issue with the UICollectionView. So here is what I found out.
I can confirm that the sectionInset is relevant to that problem. Besides that the headerReferenceSize also has influence whether a cell is deplaced or not. (This makes sense since it is needed for calcuating the origin.)
Unfortunately, even different screen sizes have to be taken into account. When playing around with the values for these two properties, I experienced that a certain configuration worked either on both (3.5" and 4"), on none, or on only one of the screen sizes. Usually none of them. (This also makes sense, since the bounds of the UICollectionView changes, therefore I did not experience any disparity between retina and non-retina.)
I ended up setting the sectionInset and headerReferenceSize depending on the screen size. I tried about 50 combinations until I found values under which the problem did not occure anymore and the layout was visually acceptable. It is very difficult to find values which work on both screen sizes.
So summarizing, I just can recommend you to play around with the values, check these on different screen sizes and hope that Apple will fix this issue.
I've just encountered a similar issue with cells disappearing after UICollectionView scroll on iOS 10 (got no problems on iOS 6-9).
Subclassing of UICollectionViewFlowLayout and overriding method layoutAttributesForElementsInRect: doesn't work in my case.
The solution was simple enough. Currently I use an instance of UICollectionViewFlowLayout and set both itemSize and estimatedItemSize (I didn't use estimatedItemSize before) and set it to some non-zero size.
Actual size is calculating in collectionView:layout:sizeForItemAtIndexPath: method.
Also, I've removed a call of invalidateLayout method from layoutSubviews in order to avoid unnecessary reloads.
I just experienced a similar issue but found a very different solution.
I am using a custom implementation of UICollectionViewFlowLayout with a horizontal scroll. I am also creating custom frame locations for each cell.
The problem that I was having was that [super layoutAttributesForElementsInRect:rect] wasn't actually returning all of the UICollectionViewLayoutAttributes that should be displayed on screen. On calls to [self.collectionView reloadData] some of the cells would suddenly be set to hidden.
What I ended up doing was to create a NSMutableDictionary that cached all of the UICollectionViewLayoutAttributes that I have seen so far and then include any items that I know should be displayed.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * attrs = [NSMutableArray array];
CGSize calculatedSize = [self calculatedItemSize];
[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];
[self.savedAttributesDict addAttribute:attr];
}
}];
// We have to do this because there is a bug in the collection view where it won't correctly return all of the on screen cells.
[self.savedAttributesDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray * cachedAttributes, BOOL *stop) {
CGFloat columnX = [key floatValue];
CGFloat leftExtreme = columnX; // This is the left edge of the element (I'm using horizontal scrolling)
CGFloat rightExtreme = columnX + calculatedSize.width; // This is the right edge of the element (I'm using horizontal scrolling)
if (leftExtreme <= (rect.origin.x + rect.size.width) || rightExtreme >= rect.origin.x) {
for (UICollectionViewLayoutAttributes * attr in cachedAttributes) {
[attrs addObject:attr];
}
}
}];
return attrs;
}
Here is the category for NSMutableDictionary that the UICollectionViewLayoutAttributes are being saved correctly.
#import "NSMutableDictionary+CDBCollectionViewAttributesCache.h"
#implementation NSMutableDictionary (CDBCollectionViewAttributesCache)
- (void)addAttribute:(UICollectionViewLayoutAttributes*)attribute {
NSString *key = [self keyForAttribute:attribute];
if (key) {
if (![self objectForKey:key]) {
NSMutableArray *array = [NSMutableArray new];
[array addObject:attribute];
[self setObject:array forKey:key];
} else {
__block BOOL alreadyExists = NO;
NSMutableArray *array = [self objectForKey:key];
[array enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *existingAttr, NSUInteger idx, BOOL *stop) {
if ([existingAttr.indexPath compare:attribute.indexPath] == NSOrderedSame) {
alreadyExists = YES;
*stop = YES;
}
}];
if (!alreadyExists) {
[array addObject:attribute];
}
}
} else {
DDLogError(#"%#", [CDKError errorWithMessage:[NSString stringWithFormat:#"Invalid UICollectionVeiwLayoutAttributes passed to category extension"] code:CDKErrorInvalidParams]);
}
}
- (NSArray*)attributesForColumn:(NSUInteger)column {
return [self objectForKey:[NSString stringWithFormat:#"%ld", column]];
}
- (void)removeAttributesForColumn:(NSUInteger)column {
[self removeObjectForKey:[NSString stringWithFormat:#"%ld", column]];
}
- (NSString*)keyForAttribute:(UICollectionViewLayoutAttributes*)attribute {
if (attribute) {
NSInteger column = (NSInteger)attribute.frame.origin.x;
return [NSString stringWithFormat:#"%ld", column];
}
return nil;
}
#end
The above answers don't work for me, but after downloading the images, I replaced
[self.yourCollectionView reloadData]
with
[self.yourCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
to refresh and it can show all cells correctly, you can try it.
This might be a little late but make sure you are setting your attributes in prepare() if possible.
My issue was that the cells were laying out, then getting update in layoutAttributesForElements. This resulted in a flicker effect when new cells came into view.
By moving all the attribute logic into prepare, then setting them in UICollectionViewCell.apply() it eliminated the flicker and created butter smooth cell displaying ๐
Swift 5 version of Nick Snyder answer:
class NDCollectionViewFlowLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var newAttributes = [AnyHashable](repeating: 0, count: attributes?.count ?? 0)
for attribute in attributes ?? [] {
if (attribute.frame.origin.x + attribute.frame.size.width <= collectionViewContentSize.width) && (attribute.frame.origin.y + attribute.frame.size.height <= collectionViewContentSize.height) {
newAttributes.append(attribute)
}
}
return newAttributes as? [UICollectionViewLayoutAttributes]
}
}
Or you could use extension of UICollectionViewFlowLayout:
extension UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var newAttributes = [AnyHashable](repeating: 0, count: attributes?.count ?? 0)
for attribute in attributes ?? [] {
if (attribute.frame.origin.x + attribute.frame.size.width <= collectionViewContentSize.width) && (attribute.frame.origin.y + attribute.frame.size.height <= collectionViewContentSize.height) {
newAttributes.append(attribute)
}
}
return newAttributes as? [UICollectionViewLayoutAttributes]
}
}
I would like to implement an app using a UIScrollView with paging, similar to the apple weather app.
But I am a little concerned about performance. The example implementation I have been using loads all of the views then the application launches. After a certain point, once this prove slow?
I wonder how Apple's camera roll is dealing with this, where a user may have 100+ photos that can be scrolled through. Should I try to figure out a way to build the view only when it is needed? Or maybe there is a way to replicate the dequeue reusable cell technique from a UITableView, only for horizontal view loading, since each view will have the same layout.
By far the most efficient solution (and this is used in many photo-browsing apps such as Facebook, and probably the native Photos app too) is going to be to load the content on-demand, just as UITableView does. Apple's StreetScroller sample project should get you on the right track.
A very efficient solution, is to make sure to reuse any views whenever possible. If you are going to be simply displaying images, you could use a subclass of UIScrollView, and layout these reusable views within layoutSubviews. Here you could detect what views are visible and not visible and create the subviews as needed.
An example dequeuing function may look like:
- (UIImageView *)dequeueReusableTileWithFrame:(CGRect) frame andImage:(UIImage *) image
{
UIImageView *tile = [reusableTiles anyObject];
if (tile) {
[reusableTiles removeObject:tile];
tile.frame = frame;
}
else {
tile = [[UIImageView alloc] initWithFrame:frame];
}
tile.image = image;
return tile;
}
Where reusableTiles is just an iVar of NSMutableSet type. You could then use this to load fetch any currently offscreen image views and quickly and easily bring them back into view.
Your layoutSubviews may look something like:
- (void)layoutSubviews {
[super layoutSubviews];
CGRect visibleBounds = [self bounds];
CGPoint contentArea = [self contentOffset];
//recycle all tiles that are not visible
for (GSVLineTileView *tile in [self subviews]) {
if (! CGRectIntersectsRect([tile frame], visibleBounds)) {
[reusableTiles addObject:tile];
[tile removeFromSuperview];
}
}
int col = firstVisibleColumn = floorf(CGRectGetMinX(visibleBounds)/tileSize.width);
lastVisibleColumn = floorf(CGRectGetMaxX(visibleBounds)/tileSize.width) ;
int row = firstVisibleRow = floorf(CGRectGetMinY(visibleBounds)/tileSize.height);
lastVisibleRow = floorf(CGRectGetMaxY(visibleBounds)/tileSize.height);
while(row <= lastVisibleRow)
{
col = firstVisibleColumn;
while (col <= lastVisibleColumn)
{
if(row < firstDisplayedRow || row > lastDisplayedRow || col < firstDisplayedColumn || col >lastDisplayedColumn)
{
UImageView* tile = [self dequeueReusableTileWithFrame:CGRectMake(tileSize.width*col, tileSize.height*row, tileSize.width, tileSize.height) andImage:YourImage];
[self addSubview:tile];
}
++col;
}
++row;
}
firstDisplayedColumn = firstVisibleColumn;
lastDisplayedColumn = lastVisibleColumn;
firstDisplayedRow = firstVisibleRow;
lastDisplayedRow = lastVisibleRow;
}
I used something similar to this to tile in areas of a line when I was working with an exceptionally large area of a scroll view and it seemed to work quite well. Sorry for any typos that I may have created when updating this for an image view instead of my custom tileView class.