UITableView with pull to refresh and locked cells like UltraVisual - ios

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!

Related

UITableViewCell setFrame "bugged" when updating table

I have a custom cell which should be spaced from the edges of the display. For that I use this:
- (void)setFrame:(CGRect)frame {
frame.origin.x += kCellSidesInset;
frame.size.width -= 2 * kCellSidesInset;
[super setFrame:frame];
}
I do have a button that hides/shows the bottom view of a stacked view inside the cell. For which I use this code:
- (IBAction)showCardDetails:(id)sender {
UITableView *cellTableView = (UITableView*)[[[[sender superview] superview] superview] superview ];
[cellTableView beginUpdates];
self.details.hidden = !self.details.hidden;
[cellTableView endUpdates];
// [cellTableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationBottom];
// [cellTableView reloadData];
}
However when the table is updated to reflect the change the right padding becomes allot bigger. Then when I scroll a bit it gets fixed.
As much as I could visually judge it is like 3 times. Maybe it adds two more kCellSidesInset on the right but I doubt it.
Why is this happening and how can it be fixed? Maybe it can be avoid by instead of giving inset to the cell giving it to the UITableView (I have some trouble figuring how to do this).
PS. All the code is inside the CustomCell.m. I am open for a suggestion to a better way of getting the UITableView inside the action. Should I use selector in the CustomTableViewController.m to implement the action there when the cell is added?
EDIT: From what I can see the re rendering of the cells goes trough three phases.
Phase one, a couple of these:
Phase two, it updates the view cells:
And here everything looks good for now. The view that I want to hide/show is hidden/shown and all is good but then some sort of cleanup breaks the layout:
I solved the problem by refactoring the setFrame method to use the superview's frame of the cell as a reference point for the cell frame
- (void)setFrame:(CGRect)frame {
frame.origin.x = self.superview.frame.origin.x + kCellSidesInset;
frame.size.width = self.superview.frame.size.width - 2 * kCellSidesInset;
[super setFrame:frame];
}

Multiple supplementary views in a UICollectionViewFlowLayout?

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:.

UICollectionView not removing old cells after scroll

I have a UICollectionView with a grid of images. When you tap on one, it opens up the grid and shows a subview with some details. Like this:
I open up the grid in my UICollectionViewLayout by adjusting the UICollectionViewLayoutAttributes and setting a translation on the transform3D property for all cells below the current row of the selected item. This works really nicely, and is a much better animation and a simpler approach than my first attempt at inserting another cell into the grid which is a different size to the others.
Anyway... it works most of the time, but then after continued use I see old images on the collection view. They are like ghost cells. I can't click them, it's like they haven't been removed from the collection view properly, and they sit on top of the cells preventing taps and just being a nuisance. Like this:
Any ideas why these cells are doing this?
EDIT:
I'd like to add, I think it only happens when i scroll the collection view really fast. I've written my own UICollectionViewFlowLayout replacement to test if it still happens. It does.
EDIT 2:
The 3d transforms or layout have nothing to do with this. It must be a bug in UICollectionView. I can exploit by just scrolling really fast, letting come to a standstill and then querying the views that are on screen. There are often double the number of cells, but they are hidden as they are stacked on top of each other. My implementation above reveals them because of the translation i do.
This can really hurt performance too.
See my answer for a solution.
My second edit of my question details why this is happenening, and here is my workaround. It's not bullet proof, but it works in my case, and if you experience somethign similar you could tweak my solution:
- (void) removeNaughtyLingeringCells {
// 1. Find the visible cells
NSArray *visibleCells = self.collectionView.visibleCells;
//NSLog(#"We have %i visible cells", visibleCells.count);
// 2. Find the visible rect of the collection view on screen now
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
//NSLog(#"Rect %#", NSStringFromCGRect(visibleRect));
// 3. Find the subviews that shouldn't be there and remove them
//NSLog(#"We have %i subviews", self.collectionView.subviews.count);
for (UIView *aView in [self.collectionView subviews]) {
if ([aView isKindOfClass:UICollectionViewCell.class]) {
CGPoint origin = aView.frame.origin;
if(CGRectContainsPoint(visibleRect, origin)) {
if (![visibleCells containsObject:aView]) {
[aView removeFromSuperview];
}
}
}
}
//NSLog(#"%i views shouldn't be there", viewsShouldntBeThere.count);
// 4. Refresh the collection view display
[self.collectionView setNeedsDisplay];
}
and
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self removeNaughtyLingeringCells];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self removeNaughtyLingeringCells];
}
A quick further comment to bandejapaisa's: under iOS 6 only, I found that UICollectionView also had a habit of bungling animated transitions. The original cells would remain where they were, copies would be made and then the copies would be animated. Usually on top of the originals but not always. So a simple bounds test wasn't sufficient.
I therefore wrote a custom subclass of UICollectionView that does the following:
- (void)didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
//
// iOS 6 contains a bug whereby it fails to remove subviews, ever as far as I can make out.
// This is a workaround for that. So, if this is iOS 6...
//
if(![UIViewController instancesRespondToSelector:#selector(automaticallyAdjustsScrollViewInsets)])
{
// ... then we'll want to wait until visibleCells has definitely been updated ...
dispatch_async(dispatch_get_main_queue(),
^{
// ... then we'll manually remove anything that's a sub of UICollectionViewCell
// and isn't currently listed as a visible cell
NSArray *visibleCells = self.visibleCells;
for(UIView *view in self.subviews)
{
if([view isKindOfClass:[UICollectionViewCell class]] && ![visibleCells containsObject:view])
[view removeFromSuperview];
}
});
}
}
Obviously it's a shame that 'is this iOS 6' test can't be a little more direct but it's hidden off in a category in my actual code.
A Swift UICollectionView extension version of bandejapaisa's answer:
extension UICollectionView {
func removeNaughtyLingeringCells() {
// 1. Find the visible cells
let visibleCells = self.visibleCells()
//NSLog("We have %i visible cells", visibleCells.count)
// 2. Find the visible rect of the collection view on screen now
let visibleRect = CGRectOffset(bounds, contentOffset.x, contentOffset.y)
//NSLog("Rect %#", NSStringFromCGRect(visibleRect))
// 3. Find the subviews that shouldn't be there and remove them
//NSLog("We have %i subviews", subviews.count)
for aView in subviews {
if let aCollectionViewCell = aView as? UICollectionViewCell {
let origin = aView.frame.origin
if (CGRectContainsPoint(visibleRect, origin)) {
if (!visibleCells.contains(aCollectionViewCell)) {
aView.removeFromSuperview()
}
}
}
}
// 4. Refresh the collection view display
setNeedsDisplay()
}
}

Reorder cells of UICollectionView

Consider a UICollectionView with flow layout and horizontal direction. By default, cells are ordered from top to bottom, left to right. Like this:
1 4 7 10 13 16
2 5 8 11 14 17
3 6 9 12 15 18
In my case, the collection view is paged and it has been designed so that a specific number of cells fits in each page. Thus, a more natural ordering would be:
1 2 3 10 11 12
4 5 6 - 13 14 15
7 8 9 16 17 18
What would be the simplest to achieve this, short of implementing my own custom layout? In particular, I don't want to loose any of the functionalities that come for free with UICollectionViewFlowLayout (such as insert/remove animations).
Or in general, how do you implement a reordering function f(n) on a flow layout? The same could be applicable to a right-to-left ordering, for example.
My approach so far
My first approach was to subclass UICollectionViewFlowLayout and override layoutAttributesForItemAtIndexPath::
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *reorderedIndexPath = [self reorderedIndexPathOfIndexPath:indexPath];
UICollectionViewLayoutAttributes *layout = [super layoutAttributesForItemAtIndexPath:reorderedIndexPath];
layout.indexPath = indexPath;
return layout;
}
Where reorderedIndexPathOfIndexPath: is f(n). By calling super, I don't have to calculate the layout of each element manually.
Additionally, I had to override layoutAttributesForElementsInRect:, which is the method the layout uses to choose which elements to display.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *result = [NSMutableArray array];
NSInteger sectionCount = 1;
if ([self.collectionView.dataSource respondsToSelector:#selector(numberOfSectionsInCollectionView:)])
{
sectionCount = [self.collectionView.dataSource numberOfSectionsInCollectionView:self.collectionView];
}
for (int s = 0; s < sectionCount; s++)
{
NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:s];
for (int i = 0; i < itemCount; i++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s];
UICollectionViewLayoutAttributes *layout = [self layoutAttributesForItemAtIndexPath:indexPath];
if (CGRectIntersectsRect(rect, layout.frame))
{
[result addObject:layout];
}
}
}
return result;
}
Here I just try every element and if it is within the given rect, I return it.
If this approach is the way to go, I have the following more specific questions:
Is there any way I can simplify the layoutAttributesForElementsInRect: override, or make it more efficient?
Am I missing something? At the very least swapping cells of different pages produces odd results. I suspect it's related to initialLayoutAttributesForAppearingItemAtIndexPath: and finalLayoutAttributesForDisappearingItemAtIndexPath:, but I can't pinpoint exactly what is the problem.
In my case, f(n) depends on the number of columns and rows of each page. Is there any way of extracting this information from UICollectionViewFlowLayout, short of hardcoding it myself? I thought of querying layoutAttributesForElementsInRect: with the bounds of the collection view, and deducing the rows and columns from there, but this also feels inefficient.
I've thought a lot about your question and came to following considerations:
Subclassing the FlowLayout seems to be the rightest and the most effective way to reorder cells and to make use of flow layout animations. And your approach works, except of two important things:
Let's say you have a collection view with only 2 cells and you have designed your page so that it can contain 9 cells. First cell will be positioned at the top left corner of the view, like in original flow layout. Your second cell, however, should be positioned at the top of the view and it has an index path [0, 1]. The reordered index path would be [0, 3] (index path of original flow layout cell that would be on its place). And in your layoutAttributesForItemAtIndexPath override you would send the message like [super layoutAttributesForItemAtIndexPath:[0, 3]], you would get an nil object, just because there are only 2 cells: [0,0] and [0,1]. And this would be the problem for your last page.
Even though you can implement the paging behavior by overriding targetContentOffsetForProposedContentOffset:withScrollingVelocity: and manually set properties like itemSize, minimumLineSpacing and minimumInteritemSpacing, it's much work to make your items be symmetrical, to define the paging borders and so on.
I thnik, subclassing the flow layout is preparing much implementation for you, because what you want is not a flow layout anymore. But let's think about it together.
Regarding your questions:
your layoutAttributesForElementsInRect: override is exactly how the original apple implementation is, so there is no way to simplify it. For your case though, you could consider following: if you have 3 rows of items per page, and the frame of item in first row intersects the rect frame, then (if all items have same size) the frames of second and third row items intersect this rect.
sorry, I didn't understand your second question
in my case the reordering function looks like this: (a is the integer number of rows/columns on every page, rows=columns)
f(n) = (n % a²) + (a - 1)(col - row) + a²(n / a²); col = (n % a²) % a; row = (n % a²) / a;
Answering the question, the flow layout has no idea how many rows are in each column because this number can vary from column to column depending on size of every item. It can also say nothing about number of columns on each page because it depends on the scrolling position and can also vary. So there is no better way than querying layoutAttributesForElementsInRect, but this will include also cells, that are only partically visible. Since your cells are equal in size, you could theoretically find out how many rows has your collection view with horizontal scrolling direction: by starting iterating each cell counting them and breaking if their frame.origin.x changes.
So, I think, you have two options to achieve your purpose:
Subclass UICollectionViewLayout. It seems to be much work implementing all those methods, but it's the only effective way. You could have for example properties like itemSize, itemsInOneRow. Then you could easily find a formula to calculate the frame of each item based on it's number (the best way is to do it in prepareLayout and store all frames in array, so that you cann access the frame you need in layoutAttributesForItemAtIndexPath). Implementing layoutAttributesForItemAtIndexPath, layoutAttributesForItemsInRect and collectionViewContentSize would be very simple as well. In initialLayoutAttributesForAppearingItemAtIndexPath and finalLayoutAttributesForDisappearingItemAtIndexPath you could just set the alpha attribute to 0.0. That's how standard flow layout animations work. By overriding targetContentOffsetForProposedContentOffset:withScrollingVelocity: you could implement the "paging behavior".
Consider making a collection view with flow layout, pagingEnabled = YES, horizontal scrolling direction and item size equal to screen size. One item per screen size. To each cell you could set a new collection view as subview with vertical flow layout and the same data source as other collection views but with an offset. It's very efficient, because then you reuse whole collection views containing blocks of 9 (or whatever) cells instead of reusing each cell with standard approach. All animations should work properly.
Here you can download a sample project using the layout subclassing approach. (#2)
Wouldn't it be a simple solution to have 2 collection views with standart UICollectionViewFlowLayout?
Or even better: to have a page view controller with horizontal scrolling, and each page would be a collection view with normal flow layout.
The idea is following: in your UICollectionViewController -init method you create a second collection view with frame offset to the right by your original collection view width. Then you add it as subview to original collection view. To switch between collection views, just add a swipe recognizer. To calculate offset values you can store the original frame of collection view in ivar cVFrame. To identify your collection views you can use tags.
Example of init method:
CGRect cVFrame = self.collectionView.frame;
UICollectionView *secondView = [[UICollectionView alloc]
initWithFrame:CGRectMake(cVFrame.origin.x + cVFrame.size.width, 0,
cVFrame.size.width, cVFrame.size.height)
collectionViewLayout:[UICollectionViewFlowLayout new]];
[secondView setBackgroundColor:[UIColor greenColor]];
[secondView setTag:1];
[secondView setDelegate:self];
[secondView setDataSource:self];
[self.collectionView addSubview:secondView];
UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:#selector(swipedRight)];
[swipeRight setDirection:UISwipeGestureRecognizerDirectionRight];
UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:#selector(swipedLeft)];
[swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft];
[self.collectionView addGestureRecognizer:swipeRight];
[self.collectionView addGestureRecognizer:swipeLeft];
Example of swipeRight and swipeLeft methods:
-(void)swipedRight {
// Switch to left collection view
[self.collectionView setContentOffset:CGPointMake(0, 0) animated:YES];
}
-(void)swipedLeft {
// Switch to right collection view
[self.collectionView setContentOffset:CGPointMake(cVFrame.size.width, 0)
animated:YES];
}
And then it's not a big problem to implement DataSource methods (in your case you want to have 9 items on each page):
-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section {
if (collectionView.tag == 1) {
// Second collection view
return self.dataArray.count % 9;
} else {
// Original collection view
return 9; // Or whatever
}
In method -collectionView:cellForRowAtIndexPath you will need to get data from your model with offset, if it's second collection view.
Also don't forget to register class for reusable cell for your second collection view as well. You can also create only one gesture recognizer and recognize swipes to the left and to the right. It's up to you.
I think, now it should work, try it out :-)
You have an object that implements the UICollectionViewDataSource protocol.
Inside collectionView:cellForItemAtIndexPath: simply return the correct item that you want to return.
I don't understand where there would be a problem.
Edit: ok, I see the problem. Here is the solution: http://www.skeuo.com/uicollectionview-custom-layout-tutorial , specifically steps 17 to 25. It's not a huge amount of work, and can be reused very easily.
This is my solution, I did not test with animations but I think it will be work well with a little changes, hope it helpful
CELLS_PER_ROW = 4;
CELLS_PER_COLUMN = 5;
CELLS_PER_PAGE = CELLS_PER_ROW * CELLS_PER_COLUMN;
UICollectionViewFlowLayout* flowLayout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;
flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
flowLayout.itemSize = new CGSize (size.width / CELLS_PER_ROW - delta, size.height / CELLS_PER_COLUMN - delta);
flowLayout.minimumInteritemSpacing = ..;
flowLayout.minimumLineSpacing = ..;
..
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index = indexPath.row;
NSInteger page = index / CELLS_PER_PAGE;
NSInteger indexInPage = index - page * CELLS_PER_PAGE;
NSInteger row = indexInPage % CELLS_PER_COLUMN;
NSInteger column = indexInPage / CELLS_PER_COLUMN;
NSInteger dataIndex = row * CELLS_PER_ROW + column + page * CELLS_PER_PAGE;
id cellData = _data[dataIndex];
...
}

UICollectionView setLayout:animated: not preserving zIndex

I've noticed that when calling setLayout:animated in a UICollectionView to switch between two layouts, the currently visible cell doesn't adhere to the zIndex it's layout attributes has been set in layoutAttributesForItemAtIndexPath:.
For example, if I were to have a UICollectionView with UICollectionViewFlowLayout, set it's minimumLineSpacing to a negative number so the cells overlap and then set a zIndex on each cell higher than that of the previous cell, then it appears as if the cells are stacked from the bottom up.
However this breaks if I set the layout to another layout then back to that original layout. It's as if the currently visible cell doesn't listen the zIndex and is placed atop the other cells. If I scroll the cell offscreen then back on it is in the correct place.
I have had the same problem. Switching the layout will disregard the zIndex for the cell.
I have managed to make it "look right" by applying a translation on the z-axis like this:
attributes.transform3D = CATransform3DMakeTranslation(0, 0, indexPath.row);
But it is just a visual fix, if you try to click on the item you will realize that the zIndex is still wrong until it is recycled by scrolling it offscreen.
I've managed to get the behaviour I'm after by using a combination grimfrog and Charlie Elliott's responses.
Charlie Elliott's solution got the correct final outcome for the items in the collection view but there was still a snapping effect on the zIndex during the animation.
grimfrog's solution provided the correct look but had the problem of the zIndex still being incorrect after the layout change, despite looking correct.
The combination of the two, while not a great solution, does work and does use the supported transform and zIndex properties of the UICollectionViewLayoutAttributes
In my layout, I have
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
[attributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) {
attributes.zIndex = attributes.indexPath.item + 1;
}];
return attributes;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForItemAtIndexPath:indexPath];
attributes.transform3D = CATransform3DMakeTranslation(0, 0, attributes.indexPath.item);
return attributes;
}
I won't make this as the correct answer just yet as I'm sure there must be another way to solve this, but I'm interested to see if this solves the problem for others as well.
Try:
// In UICollectionViewCell subclass
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
// Setting zPosition instead of relaying on
// UICollectionView zIndex management 'fixes' the issue
self.layer.zPosition = layoutAttributes.zIndex;
}
This bit me too. After several tests I realized that UICollectionView will force selected cells to be on top, regardless of the z-index.
Try setting the z-index in:
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath;
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath;
Using #sampage & #grimfrog answers as a starting point, I was able to get a similar situation working
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
attributes.zIndex = path.item;
attributes.transform3D = CATransform3DMakeTranslation(0, 0, path.item);
// other attribute settings
return attributes;
}
My layoutAttributesForElementsInRect: calls layoutAttributesForItemAtIndexPath: when generating the attribute array - so I only needed to include the zIndex and transform3D there.
I got another workaround. Since all the cells belong to the same superview, calling bringSubviewToFront : when cell displaying works. Specifically, by looking into Debug View Hierarchy, though UICollectionViewLayout not renders cells according to zIndex, it still shows cells according to the reverse order that each subview being added to it's super view.

Resources