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 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:
layoutAttributesForElementsInRect:
override, or make it more efficient?initialLayoutAttributesForAppearingItemAtIndexPath:
and finalLayoutAttributesForDisappearingItemAtIndexPath:
, but I can't pinpoint exactly what is the problem.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:
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.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:
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.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)