Left Aligning Cells in a UICollectionView

I use UICollectionView in my project, where there are several cells of different widths per line. According to: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

it distributes the cells along the entire line with equal filling. This happens as expected, except that I want them to be justified, and a hard fill width code.

I suppose I need to subclass UICollectionViewFlowLayout, however after reading some tutorials, etc. online, I just don’t understand how it works.

+78
ios objective-c uicollectionview uicollectionviewlayout
Mar 20 '14 at 16:55
source share
14 answers

Other solutions here do not work properly when a string consists of only 1 element or is complex.

In the example provided by Ryan, I changed the code to detect a new line by checking the Y position of the new element. Very simple and quick work.

Swift:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing maxY = max(layoutAttribute.frame.maxY , maxY) } return attributes } } 

Objective-C:

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributes = [super layoutAttributesForElementsInRect:rect]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. CGFloat maxY = -1.0f; //this loop assumes attributes are in IndexPath order for (UICollectionViewLayoutAttributes *attribute in attributes) { if (attribute.frame.origin.y >= maxY) { leftMargin = self.sectionInset.left; } attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height); leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing; maxY = MAX(CGRectGetMaxY(attribute.frame), maxY); } return attributes; } 
+127
Mar 15 '16 at 16:23
source share

There are many great ideas in answering this question. However, most of them have some disadvantages:

  • Solutions that do not check the value of cell y work only for single-line layouts . They are not suitable for multi-line collection layouts.
  • Solutions that check the y value, such as Angel GarcΓ­a Olloqui answer, work only if all cells have the same height . They are not suitable for variable height cells.
  • Most solutions only override the layoutAttributesForElements(in rect: CGRect) . They do not override layoutAttributesForItem(at indexPath: IndexPath) . . This is a problem because the collection view periodically calls the last function to retrieve the layout attributes for a specific index path. If you do not return the proper attributes from this function, you are likely to come across all kinds of visual errors, for example. during insertion and deletion animation of cells or when using self-contained cells by setting the layout view of the estimatedItemSize collection. Apple docs :

    It is assumed that each custom layout object will implement the layoutAttributesForItemAtIndexPath: method.

  • Many decisions also make assumptions about the rect parameter, which is passed to the layoutAttributesForElements(in rect: CGRect) . For example, many of them are based on the assumption that rect always starts at the beginning of a new line, which is optional.

So in other words:

Most of the solutions offered on this page work for some specific applications, but they do not work properly in any situation.




AlignedCollectionViewFlowLayout

To solve these problems, I created a subclass of UICollectionViewFlowLayout , which follows a similar idea proposed by matt and Chris Wagner in their answers to a similar question. It can either align cells

β¬…οΈŽ left :

Left aligned layout

or ➑︎ on the right :

Right alignment

and additionally offers options to vertically align cells in their respective rows (if they change in height).

You can simply download it here:

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

Use is straightforward and is explained in the README file. You basically create an AlignedCollectionViewFlowLayout instance, specify the desired alignment and assign it to your collectionViewLayout collectionViewLayout view:

  let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, verticalAlignment: .top) yourCollectionView.collectionViewLayout = alignedFlowLayout 

(It is also available on Cocoapods .)




How it works (for left-aligned cells):

The concept here is to rely solely on the layoutAttributesForItem(at indexPath: IndexPath) . In layoutAttributesForElements(in rect: CGRect) we just get the pointer paths of all cells in rect , and then call the first function for each index path to get the correct frames:

 override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // We may not change the original layout attributes // or UICollectionViewFlowLayout might complain. let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect)) layoutAttributesObjects?.forEach({ (layoutAttributes) in if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc. let indexPath = layoutAttributes.indexPath // Retrieve the correct frame from layoutAttributesForItem(at: indexPath): if let newFrame = layoutAttributesForItem(at: indexPath)?.frame { layoutAttributes.frame = newFrame } } }) return layoutAttributesObjects } 

(The copy() function simply creates a deep copy of all the layout attributes in the array. You can examine the source code to implement it.)

So now the only thing we need to do is to correctly implement the layoutAttributesForItem(at indexPath: IndexPath) . The superclass UICollectionViewFlowLayout already puts the correct number of cells in each row, so we only need to shift them to the left within their corresponding row. The difficulty lies in calculating the amount of space that you need to move each cell to the left.

Since we want to have a fixed spacing between cells, the basic idea is to simply assume that the previous cell (the cell to the left of the cell that is currently laid out) is already set correctly. Then we only need to add the distance between the cells to the maxX value of the previous cell frame and the origin.x value for the current cell frame.

Now we only need to know when we have reached the beginning of the row so that we do not align the cell next to the cell in the previous row. (This will lead not only to a bad layout, but also to being too lagging.) Thus, we need a recursive anchor. The approach I use to search for this recursive anchor is as follows:

To find out if the cell in the index is on the same line as the cell with index i-1 ...

  +---------+----------------------------------------------------------------+---------+ | | | | | | +------------+ | | | | | | | | | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section | | inset | |intersection| | | line rect | inset | | |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| | | (left) | | | current item | (right) | | | +------------+ | | | | previous item | | +---------+----------------------------------------------------------------+---------+ 

... I draw a rectangle around the current cell and stretch it across the width of the entire collection. Since UICollectionViewFlowLayout centers all the cells vertically, each cell on the same row should intersect with this rectangle.

Thus, I just check if the cell with index i-1 intersects this rectangle of the line created from the cell with index i.

  • If it intersects, the cell with index i is not the leftmost cell in the row.
    β†’ Get the previous frame of the cell (with index i-1) and move the current cell next to it.

  • If it does not intersect, the cell with index i is the leftmost cell in the row.

    β†’ Move the cell to the left edge of the collection view (without changing its vertical position).

I will not post the actual implementation of the layoutAttributesForItem(at indexPath: IndexPath) here, because I think the most important part is to understand the idea, and you can always check my implementation in the source code . (This is a little more complicated than explained here, because I also allow .right alignment and various vertical alignment options. However, it follows the same idea.)




Wow, I think this is the longest answer I've ever written on Stackoverflow. Hope this helps. πŸ˜‰

+41
Jun 02 '17 at 12:39 on
source share

The question arose, but there is no answer, and this is a good question. The answer is to override one method in a subclass of UICollectionViewFlowLayout:

 @implementation MYFlowLayoutSubclass //Note, the layout minimumInteritemSpacing (default 10.0) should not be less than this. #define ITEM_SPACING 10.0f - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect]; NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. //this loop assumes attributes are in IndexPath order for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) { if (attributes.frame.origin.x == self.sectionInset.left) { leftMargin = self.sectionInset.left; //will add outside loop } else { CGRect newLeftAlignedFrame = attributes.frame; newLeftAlignedFrame.origin.x = leftMargin; attributes.frame = newLeftAlignedFrame; } leftMargin += attributes.frame.size.width + ITEM_SPACING; [newAttributesForElementsInRect addObject:attributes]; } return newAttributesForElementsInRect; } @end 

As recommended by Apple, you get the layout attributes from super and iterate over them. If it is the first in the line (determined by its origin.x located on the left margin), you will leave it alone and reset x equal to zero. Then for the first cell and each cell, you add the width of that cell plus some margin. This is passed to the next element of the loop. If this is not the first element, you set its origin.x to the current calculated margin and add new elements to the array.

+20
Feb 06 '15 at 20:30
source share

I had the same problem, Try Cocoapod UICollectionViewLeftAlignedLayout . Just include it in your project and initialize it as follows:

 UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init]; UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout]; 
+9
Apr 01 '15 at 9:48
source share

Based on Michael Sand's answer , I created a subclass library, UICollectionViewFlowLayout to perform horizontal left, right, or full alignment (mostly the default) - this also allows you to set the absolute distance between each cell. I plan to add horizontal center alignment and vertical justification to it.

https://github.com/eroth/ERJustifiedFlowLayout

+5
Jun 16 '15 at 22:00
source share

Quickly. According to Michaels, answer

 override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else { return super.layoutAttributesForElementsInRect(rect) } let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED var newAttributes = [UICollectionViewLayoutAttributes]() var leftMargin = self.sectionInset.left for attributes in oldAttributes { if (attributes.frame.origin.x == self.sectionInset.left) { leftMargin = self.sectionInset.left } else { var newLeftAlignedFrame = attributes.frame newLeftAlignedFrame.origin.x = leftMargin attributes.frame = newLeftAlignedFrame } leftMargin += attributes.frame.width + spacing newAttributes.append(attributes) } return newAttributes } 
+5
Dec 12 '15 at 22:05
source share

Here is the original answer in Swift. It still works great basically.

 class LeftAlignedFlowLayout: UICollectionViewFlowLayout { private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElementsInRect(rect) var leftMargin = sectionInset.left attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } else { layoutAttribute.frame.origin.x = leftMargin } leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing } return attributes } } 

Exception: Auto-Negotiate Cells

There is one big exception sadly. If you are using UICollectionViewFlowLayout estimatedItemSize . Internally, the UICollectionViewFlowLayout changes things a bit. I did not track it completely, but its clarity called other methods after layoutAttributesForElementsInRect , while its own cell sizes. From my trial version and error, I found that for each cell, layoutAttributesForItemAtIndexPath for each cell is apparently most often called. This updated LeftAlignedFlowLayout works great with estimatedItemSize . It also works with static cells, however additional layout calls allow me to use the original answer anytime when I don't need auto-create cells.

 class LeftAlignedFlowLayout: UICollectionViewFlowLayout { private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes // First in a row. if layoutAttribute?.frame.origin.x == sectionInset.left { return layoutAttribute } // We need to align it to the previous item. let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section) guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else { return layoutAttribute } layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing return layoutAttribute } } 
+5
Mar 02 '16 at 15:36
source share

Based on all the answers, I am changing a little and it works well for me.

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY || layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } if layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } else { layoutAttribute.frame.origin.x = leftMargin } leftMargin += layoutAttribute.frame.width maxY = max(layoutAttribute.frame.maxY, maxY) } return attributes } 
+3
Mar 22 '17 at 21:43
source share

Based on the answers here, but crashes and alignment issues fixed when your collection view also has headers or footers. Align only the remaining cells:

 class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var prevMaxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in guard layoutAttribute.representedElementCategory == .cell else { return } if layoutAttribute.frame.origin.y >= prevMaxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing prevMaxY = layoutAttribute.frame.maxY } return attributes } } 
+2
Jun 28 '17 at 7:27
source share

Thanks for the answer Michael Sand . I changed it to solve several lines (the same Top y alignment of each row) as left alignment, even the distance to each element.

 static CGFloat const ITEM_SPACING = 10.0f; - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { CGRect contentRect = {CGPointZero, self.collectionViewContentSize}; NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect]; NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init]; for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) { UICollectionViewLayoutAttributes *attr = attributes.copy; CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue]; if (lastLeftMargin == 0) lastLeftMargin = leftMargin; CGRect newLeftAlignedFrame = attr.frame; newLeftAlignedFrame.origin.x = lastLeftMargin; attr.frame = newLeftAlignedFrame; lastLeftMargin += attr.frame.size.width + ITEM_SPACING; [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]]; [newAttributesForElementsInRect addObject:attr]; } return newAttributesForElementsInRect; } 
+1
May 31 '16 at 7:06 a.m.
source share

The problem with UICollectionView is that it tries to automatically place cells in an accessible area. I did this by first determining the number of rows and columns, and then determining the cell size for this row and column

1) To define sections (rows) of my UICollectionView:

 (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView 

2) Determine the number of elements in the section. You can define a different number of elements for each section. you can get the section number using the "section" parameter.

 (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 

3) Determine the cell size for each section and row separately. You can get the section number and row number using the "indexPath" parameter, i.e. [indexPath section] for section number and [indexPath row] for [indexPath row] number.

 (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath 

4) Then you can display your cells in rows and sections using:

 (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 

Note: In UICollectionView

 Section == Row IndexPath.Row == Column 
0
Feb 03 '15 at 6:57
source share

Mike Sand's answer is good, but I had some problems with this code (for example, long cells are cut out). And the new code:

 #define ITEM_SPACE 7.0f @implementation LeftAlignedCollectionViewFlowLayout - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) { if (nil == attributes.representedElementKind) { NSIndexPath* indexPath = attributes.indexPath; attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame; } } return attributesToReturn; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath]; UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset]; if (indexPath.item == 0) { // first item of section CGRect frame = currentItemAttributes.frame; frame.origin.x = sectionInset.left; // first item of the section should always be left aligned currentItemAttributes.frame = frame; return currentItemAttributes; } NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section]; CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame; CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE; CGRect currentFrame = currentItemAttributes.frame; CGRect strecthedCurrentFrame = CGRectMake(0, currentFrame.origin.y, self.collectionView.frame.size.width, currentFrame.size.height); if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line // the approach here is to take the current frame, left align it to the edge of the view // then stretch it the width of the collection view, if it intersects with the previous frame then that means it // is on the same line, otherwise it is on it own new line CGRect frame = currentItemAttributes.frame; frame.origin.x = sectionInset.left; // first item on the line should always be left aligned currentItemAttributes.frame = frame; return currentItemAttributes; } CGRect frame = currentItemAttributes.frame; frame.origin.x = previousFrameRightPoint; currentItemAttributes.frame = frame; return currentItemAttributes; } 
0
Mar 08 '17 at 14:46
source share

Edited Angel Garcia Alloca answers the respect of minimumInteritemSpacing from the collectionView(_:layout:minimumInteritemSpacingForSectionAt:) delegate collectionView(_:layout:minimumInteritemSpacingForSectionAt:) if it implements it.

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing leftMargin += layoutAttribute.frame.width + spacing maxY = max(layoutAttribute.frame.maxY , maxY) } return attributes } 
0
May 15 '17 at 17:02
source share

This code works for me. I would like to share the appropriate Swift 3.0 code.

 class SFFlowLayout: UICollectionViewFlowLayout { let itemSpacing: CGFloat = 3.0 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attriuteElementsInRect = super.layoutAttributesForElements(in: rect) var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = [] var leftMargin = self.sectionInset.left for tempAttribute in attriuteElementsInRect! { let attribute = tempAttribute if attribute.frame.origin.x == self.sectionInset.left { leftMargin = self.sectionInset.left } else { var newLeftAlignedFrame = attribute.frame newLeftAlignedFrame.origin.x = leftMargin attribute.frame = newLeftAlignedFrame } leftMargin += attribute.frame.size.width + itemSpacing newAttributeForElement.append(attribute) } return newAttributeForElement } } 
0
May 31, '17 at 8:03
source share



All Articles