I've got a fairly standard grid layout using a subclass of UICollectionViewFlowLayout. When the user taps a cell I move the cells in the subsequent rows down to make space for a detail view that will appear. It looks like this:
grid mockup
In that detail view I want to show another view with related data, similar to how iTunes shows album details. The existing layout has headers for each section and I'm currently slapping the detail view into place, manually managing the frame's position. This gets tricky with rotations and cells moving around.
How can I convince the layout to handle the detail's position by treating it as a supplementary view? My controller is configured correctly to display the detail as a supplementary view, same for the layout.
Solved the problem. In broad strokes, here's what works for me:
Create a subclass of UICollectionViewLayoutAttributes and register it in your layout by overriding the layoutAttributesClass function.
In layoutAttributesForElementsInRect: retrieve all standard layout attributes with [super layoutAttributesForElementsInRect:rect];.
Copy that array into a mutable array and append another set of attributes for the supplementary view doing something like [attributesCopy addObject:[self layoutAttributesForSupplementaryViewOfKind:YourSupplementaryKind atIndexPath:indexPathForTappedCell]];
In layoutAttributesForSupplementaryViewOfKind:atIndexPath: get the attributes for your view with something like
YourLayoutAttributes *attributes = [YourLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
Test that the elementKind matches the type of supplementary view you want to generate
Retrieve the cell's layout attributes using [self layoutAttributesForItemAtIndexPath:indexPath];
Change the frame from the cell's attributes to suit the needs of your supplementary view
Assign the new frame to the supplementary attributes
The important part to note is you can't skip subclassing UICollectionViewLayoutAttributes. Doing asking super (a UICollectionViewFlowLayout instance) for attributes of a supplemental view for anything but standard header or footers will return nil. I couldn't find any concrete documentation on this behavior so I might be wrong but in my experience it was the subclassed attributes that solved my problems.
Your code should look something like this:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allAttributesInRect = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *attributes = NSMutableArray.array;
for (UICollectionViewLayoutAttributes *cellAttributes in allAttributesInRect)
{
// Do things with regular cells and supplemental views
}
if (self.selectedCellPath)
{
UICollectionViewLayoutAttributes *detailAttributes = [self layoutAttributesForSupplementaryViewOfKind:SomeLayoutSupplimentaryDetailView atIndexPath:self.selectedCellPath];
[attributes addObject:detailAttributes];
}
return attributes.copy;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
SomeLayoutAttributes *attributes = [SomeLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath];
if ([elementKind isEqualToString:SomeLayoutSupplimentaryDetailView])
{
UICollectionViewLayoutAttributes *cellAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
CGRect frame = cellAttributes.frame;
frame.size.width = CGRectGetWidth(self.collectionView.frame); // or whatever works for you
attributes.frame = frame;
}
return attributes;
}
Your UICollectionViewLayoutAttributes subclass doesn't necessarily need any extra properties or functions but it's a great place to store data specific to that view for configuration use after you retrieve the view using dequeueReusableSupplementaryViewOfKind:forIndexPath:.
I'm trying to make something similar to what UltraVisual for iOS already does. I'd like to make my pull-to-refresh be in a cell in-between other cells.
I think the following GIF animation explains it better:
It looks like the first cell fades out when pulling up, while when you pull down and you're at the top of the table, it adds a new cell right below the first one and use it as the pull-to-refresh.
Has anyone done anything similar?
Wrote this one for UV. Its actually way simpler than you're describing. Also, for what its worth, this view was written as a UICollectionView, but the logic still applies to UITableView.
There is only one header cell. Durring the 'refresh' animation, I simply set the content inset of the UICollectionView to hold it open. Then when I've finished with the reload, I animate the content inset back to the default value.
As for the springy fixed header, there's a couple of ways you can handle it. Quick and dirty is to use a UICollectionViewFlowLayout, and modify the attributes in - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
Here's some pseudo code assuming your first cell is the sticky header:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect];
if ([self contentOffsetIsBelowZero]) {
for (UICollectionViewLayoutAttributes *attributes in layoutAttributes) {
if (attributes.indexPath.item == 0) {
CGPoint bottomOrigin = CGPointMake(0, CGRectGetMaxY(attributes.frame));
CGPoint converted = [self.collectionView convertPoint:bottomOrigin toView:self.collectionView.superview];
height = MAX(height, CGRectGetHeight(attributes.frame));
CGFloat offset = CGRectGetHeight(attributes.frame) - height;
attributes.frame = CGRectOffset(CGRectSetHeight(attributes.frame, height), 0, offset);
break;
}
}
}
Another approach would be to write a custom UICollectionViewLayout and calculate the CGRect's manually.
And finally, the 'fade out' is really nothing more than setting the opacity of the objects inside the first cell as it moves off screen. You can calculate the position of the cell on screen during - (void)applyLayoutAttributes… and set the opacity based on that.
Finally, something to note: In order to do any 'scroll based' updates with UICollectionView, you'll need to make sure - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds returns YES. You can do a simple optimisation check like:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
BOOL shouldInvalidate = [super shouldInvalidateLayoutForBoundsChange:newBounds];
if ([self contentOffsetIsBelowZero]) {
shouldInvalidate = YES;
}
return shouldInvalidate;
}
Again this is mostly pseudo code, so re-write based on your own implementation. Hope this helps!
I try to make UICollectionView with cells, that intersect and partially overlay each other as it is done at screenshot:
This layout was reached by setting
self.minimumLineSpacing = -100;
at my UICollectionViewFlowLayout subclass.
When I scroll down, everything is OK. I see what I want. But when I scroll up, I see another behaviour, not like I expected:
So my question is: how can I make my layout look as at the first screen regardless scroll view direction.
Note: I have to support both iOS 6 and 7.
Thanks very much for any advices and any help.
Hmm, interesting. Since the collection view recycles cells, they are continuously added to and removed from the view hierarchy as they move on and off the screen. That being said, it stands to reason and when they are re-added to the view, they are simply added as subviews meaning that when a cell gets recycled, it now has the highest z-index of all of the cells.
One fairly pain-free way to rectify this would be to manually adjust the z position of each cell to be incrementally higher with the index path. That way, lower (y) cells will always appear above (z) the cells above (y) them.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = #"CELLID";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
if (cell.layer.zPosition != indexPath.row) {
[cell.layer setZPosition:indexPath.row];
}
return cell;
}
Found another sollution to solve this problem. We need to use UICollectionViewFlowLayout subclass.
#interface MyFlowLayout : UICollectionViewFlowLayout
#end
#implementation MyFlowLayout
- (void)prepareLayout {
[super prepareLayout];
// This allows us to make intersection and overlapping
self.minimumLineSpacing = -100;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *currentLayoutAttributes in layoutAttributes) {
// Change zIndex allows us to change not only visible position, but logic too
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row;
}
return layoutAttributes;
}
#end
Hope that helps someone else.
Found this strange behavior while implementing a custom UICollectionViewLayout subclass. I set up the subclass methods except for collectionViewContentSize. All the cells showed up where I expected, but the contentView was too long. Looked to be about double what it should be.
I implemented the method below to get the correct contentSize. Though, it's now the expected value, prepareLayout is called every single time the view scrolls one pixel. That means if I swipe from 0,0 to 0,500, prepareLayout is called 500 times!
What is it about my collectionViewContentSize that could cause that?
- (CGSize)collectionViewContentSize {
UICollectionViewLayoutAttributes *leftAttributes = (UICollectionViewLayoutAttributes *)self.layoutInfo[#"segmentCell"][[NSIndexPath indexPathForItem:[self.collectionView numberOfItemsInSection:0]-1 inSection:0]];
UICollectionViewLayoutAttributes *rightAttributes = (UICollectionViewLayoutAttributes *)self.layoutInfo[#"segmentCell"][[NSIndexPath indexPathForItem:[self.collectionView numberOfItemsInSection:1]-1 inSection:1]];
float leftHeight = leftAttributes.frame.size.height + leftAttributes.frame.origin.y;
float rightHeight = rightAttributes.frame.size.height + rightAttributes.frame.origin.y;
float maxHeight = leftHeight > rightHeight ? leftHeight : rightHeight;
return CGSizeMake(self.collectionView.bounds.size.width, maxHeight);
}
Although according to the docs [0] shoulInvalidateLayoutForBoundsChange: is supposed to return NO by default, it wasn't. Once I implemented it and had it return NO in all cases, prepareLayout is no longer called with every bounds change. That seems like a bug in UICollectionViewLayout.
[0] http://developer.apple.com/library/ios/#documentation/uikit/reference/UICollectionViewLayout_class/Reference/Reference.html
A reason why you would want this to happen is that when using a custom layout, you may want the size of the cell to change depending on the content offset such as when you swipe up on an iPhone X where the Y value determines the size of the cell. This saves you from having to deal with separate gesture recognisers for a particular effect you might want.
What's happening
Currently I have an application that uses two UICollectionViews inside a UITableView. This way I create a Pulse News look like application. My problem with this is that sometimes the 6th and 11th row disappears completely, leaving a blank space where it should be the cell. I wouldn't actually mind, if all the cells were like this (and this way I could assume that I wasn't doing things correctly), but the thing is, is just happening with those specific ones.
My theory
The 6th and 11th rows are the ones that appears when I start scrolling, so by default I am able to see 5 cells, and when I do the first horizontal scrolling the 6th comes up (blank space sometimes).
What I have
The only thing I am doing at the moment is this:
[self.collectionView registerNib:[UINib nibWithNibName:CELL_NIB_NAME bundle:nil] forCellWithReuseIdentifier:CELL_IDENTIFIER];
On the viewDidLoad. And on the creation of the cell:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CELL_IDENTIFIER forIndexPath:indexPath];
NSMutableDictionary *dictionary = [self.DataSource objectAtIndex:[indexPath row]];
[cell buildViewWithDictionary:dictionary withReferenceParent:self.referenceViewController];
return cell;
}
So on my understating nothing fancy going on here. I though there was something wrong on the data source (a dummy JSON file), but sometimes it works ok and the cell shows, so I guess from that part is ok.
So my "question": Does anyone knows what's going on? I don't really like to say that it's a bug from iOS, but I can't think of anything else.
Edit 1.0
The amazing part is that this method
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
Is going from indexPath [0,4] to [0,6] without calculating the [0,5]. First time I actually see this happening in iOS.
Edit 2.0
I have switched the way I am creating the cells, and instead of dequeuing I am using the old way:
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CELL_NIB_NAME owner:self options:nil];
MyCell *cell = (MyCell *)[nib objectAtIndex:0];
Still the same sad result.
So, what did work?
1) Subclass UICollectionViewFlowLayout.
2) Set the flowLayout of my UICollectionView to my new subclass.
3) On the init method of the UICollectionViewFlowLayout subclass, set the orientation you want:
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
In my case it is Horizontal.
4) The important part:
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
At this moment, I should theorise a bit, but honestly I don't have a clue.
The above answers didn't work for me, but after downloading the images, I replaced the code
[self.myCollectionView reloadData]
with
[self.myCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
to refresh the collectionview and it shows all cells, you can try it.
None of the solutions given by anyone helped me in my custom layout that we need to have in our app.
Instead, I had to do this: (And yeah, IT WORKS!!!)
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
CGSize size = [self collectionViewContentSize];
rect.size.height = size.height*2;
NSArray *atrributes_Super = [super layoutAttributesForElementsInRect:rect];
return atrributes_Super;
}
After all, UICollectionView is just looking for the attributes for the elements to be displayed in your screen's rect.
Rob's tip about the bug helped me. The bug states that if the section insets and cells widths and spacing add up exactly to the width of the screen then the first column sometimes randomly dissappears and reappears for some cells in some places. Rather than subclass or change the cell widths, I changed the section insets for left and right in my storyboard from 6 to 4 and it I haven't seen the problem again.
As I run the same problem suddenly and spent some time figuring out one of possible reasons of cell disappearing during the scroll, I will add my answer as well.
Prerequisites of the problem:
You have a UICollectionView instance
You have a UICollectionViewFlowLayoutSubclass
The problem
Cells disappear from the Collection View after scrolling to the certain point.
The source of the problem is in wrong subclassing of the UICollectionViewFlowLayout.
As it explicitly said in documentation:
Every layout object should implement the following methods:
- collectionViewContentSize
- layoutAttributesForElements(in:)
- layoutAttributesForItem(at:)
- layoutAttributesForSupplementaryView(ofKind:at:) // (if your layout supports -supplementary views)
-layoutAttributesForDecorationView(ofKind:at:) // (if your layout supports decoration views)
- shouldInvalidateLayout(forBoundsChange:)
By relying on UICollectionViewFlowLayout implementation of methods above we miss the fact, that func layoutAttributesForElements(in rect: CGRect) and collectionViewContentSize will generate wrong contentSize (the size that would be correct if all the cells would have itemSize size and the content size would be corresponding. As soon as scroll offsetY will be greater that contentSize height cell will all disappear.
The solution
The solution is in proper UICollectionViewFlowLayout subclassing. Override all the methods that are required to override and everything will work just fine.
In my case (vertical scroll, with cells disappearing in first view), cells were disappearing due to incorrect estimated size. It seems, UICollectionView uses the estimated size to calculate the items to load in first view. I'd set the estimated size too high which was resulting in wrong calculations for number of items to load in first screen.
The moment I made the estimated height bit low, all the cells appeared correctly.
We ran into disappearing cells recently and found that rather than skipping 'hidden' cells we were accidentally inserting 0x0 sized cells. The resulting behavior was very confusing and did not suggest these invisible cells were the issue. We would see the correctly sized cells and layout, but a few of the valid cells would consistently disappear after scrolling off/on screen. I have no idea why intermingling 0 sized cells would cause this behavior, but removing them fixed the problem. This may not be causing your problem, but this may be helpful to devs searching for similar symptoms.
Just ran into an issue where all UICollectionView cells were disappearing on scroll.
This happened because I had declared
extension UICollectionViewLayout {
static let defaultLayout: UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
return layout
}()
}
... meaning the same layout instance was being used in multiple UICollectionViews. I had meant to make that a computed var. Hope this helps someone who's accidentally using the same layout object in multiple collection views.
What caused the cells to disappear in my case was that the data source was being deallocated prematurely. UICollectionView.dataSource is a weak property, which means that unless you keep a strong reference to it, the object will be deallocated at the end of the scope in which you created. The problem manifested itself with disappearing cells as soon as I tapped on the UICollectionView.
For me this issue seemed to be related with the way i make my collectionview adapt to an open keyboard to prevent content overlaps.
in my observer to respond to KeyboardWillShow i had this:
var userInfo = obj.UserInfo[UIKeyboard.FrameEndUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
var windowOffset = this.Superview.ConvertPointToView(this.Frame.Location, UIApplication.SharedApplication.KeyWindow);
var newHeight = rect.Y - windowOffset.Y;
this._controller.CollectionView.Frame = new CGRect(0, 0, this._controller.CollectionView.Frame.Width, newHeight);
}
After changing it to this:
var userInfo = obj.UserInfo[UIKeyboard.FrameBeginUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
UIEdgeInsets contentInsets = new UIEdgeInsets(0, 0, rect.Height, 0);
this._controller.CollectionView.ContentInset = contentInsets;
this._controller.CollectionView.ScrollIndicatorInsets = contentInsets;
}
The cell disappearance issue completely went away. This is C# from working with xamarin but i hope it helps someone else.
I think this is not a UICollectionView‘s bug, maybe your not return right data in - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect method.
You can see this demo: https://github.com/lqcjdx/YLTagsChooser , all cells can appear when scolling the UICollectionView.