UICollectionView 中的左对齐单元格

我在项目中使用了 UICollectionView,其中在一条线上有多个宽度不同的单元格。根据: Https://developer.apple.com/library/content/documentation/windowsviews/conceptual/collectionviewpgforios/usingtheflowlayout/usingtheflowlayout.html

它用相同的填充物将细胞分散在整条线上。这发生在意料之中,除了我想左对齐他们,硬代码一个填充宽度。

I figure I need to subclass UICollectionViewFlowLayout, however after reading some of the tutorials etc online I just don't seem to get how this works.

85581 次浏览

UICollectionView 的问题在于它试图自动适应可用区域中的单元格。 为此,我首先定义了行和列的数量,然后定义了该行和列的单元格大小

1) To define Sections (Rows) of my UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2)定义一节中的项目数。您可以为每个部分定义不同数量的项。可以使用‘ section’参数获取节号。

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

3)分别为每个区段和行定义单元格大小。您可以使用‘ indexPath’参数获得部分编号和行号,例如,[indexPath section]表示部分编号,[indexPath row]表示行号。

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

4)然后你可以使用以下方法将你的单元格分行分段显示:

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

NOTE: 在 UICollectionView 中

Section == Row
IndexPath.Row == Column

这个问题已经提出了一段时间,但没有答案,这是一个很好的问题。答案是重写 UICollectionViewFlowLayout 子类中的一个方法:

@implementation MYFlowLayoutSubclass


//Note, the layout's 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

根据 Apple 的建议,您可以从 super 获得布局属性,并对它们进行迭代。如果它是行中的第一个(由左边距处的 Orig.x 定义) ,那么不要管它,将 x 重置为零。然后,对于第一个单元格和每个单元格,添加该单元格的宽度和一些边距。这将传递给循环中的下一个项。如果它不是第一个项目,那么您将它的 Orig.x 设置为正在运行的计算边距,并向数组中添加新元素。

I had the same problem, 尝试一下 Cocoapod UICollectionViewLeftAlignedLayout,只需要将它包含在你的项目中,然后像这样初始化它:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];

Building on Michael Sand 的回答, I created a subclassed UICollectionViewFlowLayout library to do left, right, or full (basically the default) horizontal justification—it also lets you set the absolute distance between each cell. I plan on adding horizontal center justification and vertical justification to it, too.

https://github.com/eroth/ERJustifiedFlowLayout

很快,根据迈克尔斯的回答

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
}

这是 Swift 中的原始答案,大多数情况下仍然很好用。

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
}
}

异常: 自动调整单元格大小

遗憾的是,还有一个很大的例外。如果你使用的是 UICollectionViewFlowLayoutestimatedItemSize。内部 UICollectionViewFlowLayout正在做出一些改变。我还没有完全跟踪到它,但是很清楚它在 layoutAttributesForElementsInRect之后调用其他方法,同时自我调整单元格大小。从我的尝试和错误,我发现它似乎调用 layoutAttributesForItemAtIndexPath为每个细胞单独在自动调整更频繁。这个更新的 LeftAlignedFlowLayoutestimatedItemSize工作得非常好。它同样适用于静态大小的单元格,但是额外的布局调用使我在不需要自动调整单元格的任何时候都可以使用原始的答案。

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
}
}

The other solutions in this thread do not work properly, when the line is composed by only 1 item or are over complicated.

基于 Ryan 给出的示例,我更改了代码,通过检查新元素的 Y 位置来检测新行。非常简单和快速的性能。

斯威夫特:

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
}
}

If you want to have supplementary views keep their size, add the following at the top of the closure in the forEach call:

guard layoutAttribute.representedElementCategory == .cell else {
return
}

目标 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;
}

谢谢你的 Michael Sand 的回答。我将它修改为一个多行(每行的同一对齐方式)的解决方案,即左对齐,甚至每个项的间距。

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;
}

Mike Sand 的回答很好,但是我在这段代码中遇到了一些问题(比如删除了冗长的单元格)。新代码:

#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's 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;
}

基于所有的答案,我改变了一点,这对我有好处

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
}

Edited Angel García Olloqui's answer to respect minimumInteritemSpacing from delegate's 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
}

上面的代码对我很有用,我想分享一下相应的 Swift 3.0代码。

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
}
}

这个问题的答案中包含了许多伟大的想法,然而,它们中的大多数都有一些缺点:

  • 不检查单元格 只适用于单行布局的解决方案。它们对于具有多行的集合视图布局失败。

  • 检查 值的解决方案,如 安吉尔 · 加西亚 · 奥洛基的回答 只有当所有单元格具有相同的高度时才起作用。对于具有可变高度的单元格,它们失败。

  • 大多数解决方案只覆盖 layoutAttributesForElements(in rect: CGRect)函数。这是一个问题,因为集合视图定期调用后一个函数来检索特定索引路径的布局属性。如果你没有从这个函数返回正确的属性,你很可能会遇到各种各样的视觉错误,例如在单元格的插入和删除动画或者通过设置集合视图布局的 estimatedItemSize来自动调整单元格大小。 苹果文档州:

    每个自定义布局对象都应该实现 layoutAttributesForItemAtIndexPath:方法。

  • 许多解决方案还对传递给 layoutAttributesForElements(in rect: CGRect)函数的 rect参数进行假设。例如,许多基于这样的假设,即 rect总是从新行的开头开始,这种情况并不一定。

换句话说:

本页面中提供的大多数解决方案都适用于某些特定的应用程序,但它们并非在所有情况下都能如预期那样工作。


AlignedCollectionViewFlowLayout

为了解决这些问题,我创建了一个 UICollectionViewFlowLayout子类,它遵循 matt克里斯 · 瓦格纳在回答类似问题时提出的类似想法。它可以让细胞对齐

返回文章页面

Left-aligned layout

or ➡︎ :

Right-aligned layout

and additionally offers options to vertically align the cells in their respective rows (in case they vary in height).

You can simply download it here:

Https://github.com/mischa-hildebrand/alignedcollectionviewflowlayout

其用法很简单,并在 README 文件中进行了说明。您基本上创建了 AlignedCollectionViewFlowLayout的一个实例,指定所需的对齐方式,并将其分配给集合视图的 collectionViewLayout属性:

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


yourCollectionView.collectionViewLayout = alignedFlowLayout

(也可在 Cocoapods上下载。)


它是如何工作的(对于左对齐的单元格) :

这里的概念是依靠 完全依赖于 layoutAttributesForItem(at indexPath: IndexPath)函数。在 layoutAttributesForElements(in rect: CGRect)中,我们只需获取 rect中所有单元格的索引路径,然后调用每个索引路径的第一个函数来检索正确的帧:

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
}

(copy()函数只是创建数组中所有布局属性的深拷贝。您可以查看 源代码以了解其实现。)

所以现在我们要做的唯一一件事就是正确地实现 layoutAttributesForItem(at indexPath: IndexPath)函数。超类 UICollectionViewFlowLayout已经在每一行中放置了正确的单元格数量,因此我们只需要将它们移动到各自行中的左边。困难在于计算将每个单元格向左移动所需的空间量。

因为我们希望在单元格之间有一个固定的间距,所以核心思想就是假设前一个单元格(当前布局的单元格的左边)已经被正确定位。然后,我们只需要将单元格间距添加到前一个单元格框架的 maxX值,即当前单元格框架的 origin.x值。

现在我们只需要知道什么时候我们已经到达了一行的开头,这样我们就不会把一个单元格排列在前一行的单元格旁边。(这不仅会导致错误的布局,而且会造成极大的滞后。)所以我们需要一个递归锚。我用来寻找递归锚的方法如下:

要查明位于 i索引处的单元格是否与位于 I-1索引处的单元格在同一行..。

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

... 我在当前单元格周围“绘制”一个矩形,并将其拉伸到整个集合视图的宽度。由于 UICollectionViewFlowLayout的中心所有细胞垂直每个细胞在同一行 must与这个矩形相交。

因此,我只需检查具有 I-1索引的单元格是否与从具有 索引的单元格创建的这个线性矩形相交。

  • 如果它确实相交,则具有 索引的单元格不是该行中最左边的单元格。
    获取前一个单元格的框架(带有 i−1索引)并将当前单元格移动到它旁边。

  • If it does not intersect, the cell with index is the left most cell in the line.
    → Move the cell to the left edge of the collection view (without changing its vertical position).

我不会在这里发布 layoutAttributesForItem(at indexPath: IndexPath)函数的实际实现,因为我认为最重要的部分是理解 主意,您可以在 源代码中检查我的实现。(这比这里解释的要复杂一些,因为我还允许 .right对齐和各种垂直对齐选项。然而,它遵循了同样的思想。)


哇,我想这是我写过的关于 Stackoverflow 的最长的答案。我希望这能有所帮助。

Based on answers here, but fixed crashes and aligning problems when your collection view is also got headers or footers. Aligning left only 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
}
}

With Swift 4.1 and iOS 11, according to your needs, you may choose one of the 2在完成实施后 in order to fix your problem.


左对齐自动调整 UICollectionViewCell的大小

下面的实现展示了如何使用 UICollectionViewLayoutlayoutAttributesForElements(in:)UICollectionViewFlowLayoutestimatedItemSizeUILabelpreferredMaxLayoutWidth,以便在 UICollectionView中保持对齐的自动调整单元格:

CollectionViewController Swift

import UIKit


class CollectionViewController: UICollectionViewController {


let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]


let columnLayout = FlowLayout(
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)


override func viewDidLoad() {
super.viewDidLoad()


collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}


override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}


override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
cell.label.text = array[indexPath.row]
return cell
}


override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}


}

FlowLayout Swift

import UIKit


class FlowLayout: UICollectionViewFlowLayout {


required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()


estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }


// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })


// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left


// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}


return layoutAttributes
}


}

CollectionViewCell Swift

import UIKit


class CollectionViewCell: UICollectionViewCell {


let label = UILabel()


override init(frame: CGRect) {
super.init(frame: frame)


contentView.backgroundColor = .orange
label.preferredMaxLayoutWidth = 120
label.numberOfLines = 0


contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


}

预期结果:

enter image description here


左对齐固定大小的 UICollectionViewCell

The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:) and UICollectionViewFlowLayout's itemSize in order to left align cells with predefined size in a UICollectionView:

CollectionViewController Swift

import UIKit


class CollectionViewController: UICollectionViewController {


let columnLayout = FlowLayout(
itemSize: CGSize(width: 140, height: 140),
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)


override func viewDidLoad() {
super.viewDidLoad()


collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}


override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}


override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}


override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}


}

FlowLayout Swift

import UIKit


class FlowLayout: UICollectionViewFlowLayout {


required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()


self.itemSize = itemSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }


// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })


// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left


// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}


return layoutAttributes
}


}

CollectionViewCell Swift

import UIKit


class CollectionViewCell: UICollectionViewCell {


override init(frame: CGRect) {
super.init(frame: frame)


contentView.backgroundColor = .cyan
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


}

预期结果:

enter image description here

如果你们中的任何人面临问题-位于集合视图右侧的一些单元格超出了集合视图的界限。 那就用这个

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)


var leftMargin : CGFloat = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
leftMargin = sectionInset.left
}


layoutAttribute.frame.origin.x = leftMargin


leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}

使用 内景代替比较 CGFloat值。

2019年的简单解决方案

这是一个令人沮丧的问题,这些年来事情已经发生了很大的变化。现在很容易。

Basically you just do this:

    // as you move across one row ...
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
// and, obviously start fresh again each row

现在您只需要样板代码:

override func layoutAttributesForElements(
in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
    

guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
    

for a in att {
if a.representedElementCategory != .cell { continue }
        

if a.frame.origin.y >= y { x = sectionInset.left }
        

a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
        

y = a.frame.maxY
}
return att
}

只要复制粘贴到 UICollectionViewFlowLayout-你就完成了。

复制和粘贴完整工作解决方案:

This is the whole thing:

class TagsLayout: UICollectionViewFlowLayout {
    

required override init() {super.init(); common()}
required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
    

private func common() {
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
minimumLineSpacing = 10
minimumInteritemSpacing = 10
}
    

override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        

guard let att = super.layoutAttributesForElements(in:rect) else {return []}
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
        

for a in att {
if a.representedElementCategory != .cell { continue }
            

if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}

enter image description here

最后。

感谢上面@AlexShubin 首先澄清了这一点!

An important issue:

I've found that in some cases - this could be a 2022 Catalyst bug or issue - that strangely you can't reliably set minimumLineSpacing / minimumInteritemSpacing in the bringup code.

我现在简单地把这些代码行放在这里

class AlignLeftLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
minimumLineSpacing = 10
minimumInteritemSpacing = 10

这可能对某人有帮助。

简单地说,单列布局对于任何人来说都是一个重要的提示。

简单地执行“每行一项”集合视图是非常困难的。有很多不同的方法来解决这个问题。这里有一个极其简单可靠的方法:

///The formula for a "one item per line" layout.
class SingleColumnLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
minimumLineSpacing = 10
minimumInteritemSpacing = CGFloat.greatestFiniteMagnitude
            

guard let att = super.layoutAttributesForElements(in:rect) else {return []}
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
            

for a in att {
if a.representedElementCategory != .cell { continue }
                

if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}

注意 CGFloat.greatestFiniteMagnitude

以下是我寻找与 Swift 5一起工作的最佳代码的过程。我从这个帖子和其他帖子中加入了几个答案,以解决我面临的警告和问题。在滚动我的集合视图时,出现了一个警告和一些异常行为。控制台输出以下内容:

这可能是因为流布局“ xyz”正在修改 UICollectionViewFlowLayout 返回的属性,而没有复制它们。

我遇到的另一个问题是,一些冗长的单元格在屏幕的右侧被裁剪掉了。另外,我在委托函数中设置了部分插入和最小 InteritemSpaces,这导致了值没有反映在自定义类中。解决办法是在将这些属性应用到我的集合视图之前将它们设置为布局的实例。

下面是我如何使用我的集合视图的布局:

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

下面是流布局类

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }


var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}


if Int(layoutAttribute.frame.origin.y) >= Int(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 + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}


return attributes
}
}

基于所有的答案。 为 left ToRight 和 right ToLeft 工作

class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
let attributes = super.layoutAttributesForElements(in: rect)


let ltr = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight
var leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY
{
leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
}


layoutAttribute.frame.origin.x = leftMargin - (ltr ? 0 : layoutAttribute.frame.width)


if (ltr)
{
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
}
else
{
leftMargin -= layoutAttribute.frame.width + minimumInteritemSpacing
}
maxY = max(layoutAttribute.frame.maxY , maxY)
}


return attributes
}
}

如果您的最低部署目标是 iOS13,我强烈建议您利用 组合布局(doc 给你,WWDC 演示文稿 here)。

我最初确实尝试了这里的一些顶级答案。不幸的是,我们遇到了一个问题,其中一些细胞往往间歇性消失。对我们来说,这发生在调用 UICollectionView 的 reloadData函数之后。值得注意的是,我们的单元格具有可变宽度,也就是自动调整大小。

Let me show you an example. Let's say we need to display a page with a list of keyword bubbles.

enter image description here

下面是您可能需要使用组合布局来完成的内容。

override func viewDidLoad() {
super.viewDidLoad()
...
collectionView.collectionViewLayout = createLeftAlignedLayout()
}


private func createLeftAlignedLayout() -> UICollectionViewLayout {
let item = NSCollectionLayoutItem(          // this is your cell
layoutSize: NSCollectionLayoutSize(
widthDimension: .estimated(40),         // variable width
heightDimension: .absolute(48)          // fixed height
)
)
  

let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1.0),  // 100% width as inset by its Section
heightDimension: .estimated(50)         // variable height; allows for multiple rows of items
),
subitems: [item]
)
group.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
group.interItemSpacing = .fixed(10)         // horizontal spacing between cells
  

return UICollectionViewCompositionalLayout(section: .init(group: group))
}

如你所见,这很简单。

这个页面上的大多数解决方案都太复杂了。左对齐它们的最简单方法,即使只有一个单元格,也是返回以下边缘插入:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {


if collectionView.numberOfItems(inSection: section) == 1 {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: collectionView.frame.width - flowLayout.itemSize.width)
} else {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}

截至2021年1月,

Angel 的回答仍然有意义。您只需要创建一个自定义流布局(并将 Collectionview 设置为使用该自定义流布局) ,但是您需要向该自定义类添加的唯一内容是这个方法(在目标 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;
}

我在应用程序中使用了更复杂的设计,其中 tableView 单元格可能有 CollectionView,所以我得到的最佳解决方案不是硬编码,而是使用了这个库:

Https://github.com/rubygarage/collection-view-layouts

and here's my implementation:

import collection_view_layouts
class CIFilterBubblesCell: UITableViewCell, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, LayoutDelegate{


@IBOutlet weak var filterBubblesCollectionView: UICollectionView!


var bubbleFilters : [BubbleViewData] = []
let cellID = "BubbleCellID"
let cellName = "BubbleCell"


private var cellSizes = [[CGSize]]()


override func awakeFromNib() {
super.awakeFromNib()
}


func setupCell(data: [BubbleViewData])
{
    

bubbleFilters = data
prepareCellSizes()
    

filterBubblesCollectionView.register(UINib(nibName: cellName, bundle: nil), forCellWithReuseIdentifier: cellID)
    

filterBubblesCollectionView.delegate = self
filterBubblesCollectionView.dataSource = self


let layout: TagsLayout = TagsLayout()
layout.delegate = self
layout.cellsPadding = ItemsPadding(horizontal: 5, vertical: 5)
layout.contentAlign = .left
filterBubblesCollectionView.collectionViewLayout = layout
filterBubblesCollectionView.reloadData()
    

self.layoutIfNeeded()
}


override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}


func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
bubbleFilters.count
}


func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//...
}


func cellSize(indexPath: IndexPath) -> CGSize {
return cellSizes[indexPath.section][indexPath.row]
}


private func prepareCellSizes() {
cellSizes.removeAll()
var sizes: [CGSize] = []
bubbleFilters.forEach {item in
var size = item.name.sizeOfString(usingFont: UIFont.systemFont(ofSize: 17))
size.width += 30
size.height += 10
        

if (UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad)
{
size.width += 70
size.height += 10
}
        

sizes.append(size)
}
cellSizes.append(sizes)
}


}

不要忘记添加这个扩展:

extension String {
func sizeOfString(usingFont font: UIFont) -> CGSize {
let fontAttributes = [NSAttributedString.Key.font: font]
return self.size(withAttributes: fontAttributes)
}
}

如果您的 not serious custom view任何您可以设置 Scroll Direction水平取代 垂直然后尝试构建运行的项目单元格将开始在左侧而不是中心

enter image description here