在UICollectionView中左对齐单元格

时间:2014-03-20 16:55:36

标签: ios objective-c uicollectionview uicollectionviewlayout

我在我的项目中使用UICollectionView,其中一行中有多个不同宽度的单元格。根据: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

它使用相同的填充将细胞扩散到整个线上。这种情况按预期发生,除了我想左对齐它们,并硬编码填充宽度。

我想我需要继承UICollectionViewFlowLayout,但是在线阅读了一些教程之后,我似乎无法了解其工作原理。

22 个答案:

答案 0 :(得分:114)

当线条仅由1个项目组成或者过于复杂时,此处的其他解决方案无法正常工作。

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

如果您希望补充视图保持其大小,请在forEach调用中的闭包顶部添加以下内容:

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

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

答案 1 :(得分:34)

这个问题的答案中包含许多好主意。但是,大多数都有一些缺点:

  • 不检查单元格 y 的解决方案仅适用于单行布局。对于具有多行的集合视图布局,它们会失败。
  • 执行检查 y 值的解决方案,如Angel García Olloqui's answer 仅在所有单元格具有相同高度时才有效。对于高度可变的细胞,它们会失败。
  • 大多数解决方案仅覆盖layoutAttributesForElements(in rect: CGRect)功能。 它们不会覆盖layoutAttributesForItem(at indexPath: IndexPath)这是一个问题,因为集合视图会定期调用后一个函数来检索特定索引路径的布局属性。如果您没有从该功能返回正确的属性,则可能会遇到所有类型的视觉错误,例如:在插入和删除单元格动画期间或使用自定义单元格时,通过设置集合视图布局estimatedItemSizeApple docs州:

      

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

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

换句话说:

此页面上建议的大多数解决方案适用于某些特定应用程序,但它们在任何情况下都无法按预期工作。

AlignedCollectionViewFlowLayout

为了解决这些问题,我创建了一个UICollectionViewFlowLayout子类,它遵循mattChris Wagner在类似问题的答案中提出的类似想法。它可以对齐细胞

⬅︎

Left-aligned layout

➡︎ 正确

Right-aligned layout

并另外提供垂直对齐各个行中的单元格的选项(如果它们的高度不同)。

您可以在此处下载:

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()函数只是创建数组中所有布局属性的深层副本。您可以查看source code的实现。)

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

由于我们希望在单元格之间有一个固定的间距,因此核心思想是假设前一个单元格(当前布局的单元格左侧的单元格)已经正确定位。然后,我们只需要将单元格间距添加到前一个单元格框架的maxX值,并且该值是当前单元格框架的origin.x值。 / p>

现在我们只需要知道我们何时到达一行的开头,这样我们就不会在前一行的单元格旁边对齐一个单元格。 (这不仅会导致布局不正确,而且还会非常滞后。)因此我们需要一个递归锚点。我用于查找递归锚点的方法如下:

要确定索引 i 的单元格是否与索引为 i-1 的单元格位于同一行...

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

...我"画"当前单元格周围的矩形,并将其拉伸到整个集合视图的宽度上。当UICollectionViewFlowLayout垂直居中所有单元格时,同一行中的每个单元格必须与此矩形相交。

因此,我只是检查索引 i-1 的单元格是否与从索引为 i 的单元格创建的此行矩形相交。

  • 如果它相交,则索引 i 的单元格不是该行中最左边的单元格。
    →获取上一个单元格的框架(索引 i-1 )并移动旁边的当前单元格。

  • 如果它不相交,则索引为 i 的单元格是该行中最左边的单元格。
    →将单元格移动到集合视图的左边缘(不更改其垂直位置)。

我不会在这里发布layoutAttributesForItem(at indexPath: IndexPath)函数的实际实现,因为我认为最重要的部分是理解的想法,你总是可以检查我的实现source code。 (它比这里解释的要复杂一点,因为我也允许.right对齐和各种垂直对齐选项。但是,它遵循相同的想法。)

哇,我想这是我在Stackoverflow上写过的最长的答案。我希望这有帮助。

答案 2 :(得分:20)

问题已经持续了一段时间,但没有答案,这是一个很好的问题。答案是覆盖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获取布局属性并迭代它们。如果它是行中的第一个(由其origin.x定义在左边距上),则不管它并将x重置为零。然后对于第一个单元格和每个单元格,添加该单元格的宽度加上一些边距。这将传递给循环中的下一个项目。如果它不是第一项,则将其原始x设置为正在运行的计算边距,并将新元素添加到数组中。

答案 3 :(得分:17)

使用Swift 4.1和iOS 11,根据您的需要,您可以选择 2以下完整实施之一来解决您的问题。

#1。左对齐自动调整UICollectionViewCell s

以下实施说明如何使用UICollectionViewLayout的{​​{3}},UICollectionViewFlowLayout的{​​{3}}和UILabel的{​​{3}}按顺序排列左对齐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")
    }

}

预期结果:

layoutAttributesForElements(in:)

#2。左对齐UICollectionViewCell s,大小固定

下面的实现显示了如何使用UICollectionViewLayout的{​​{3}}和UICollectionViewFlowLayout的{​​{3}},以便在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")
    }

}

预期结果:

estimatedItemSize

答案 4 :(得分:10)

我有同样的问题, 试试Cocoapod UICollectionViewLeftAlignedLayout。只需将它包含在您的项目中并按如下所示进行初始化:

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

答案 5 :(得分:5)

Michael Sand's answer的基础上,我创建了一个子类化UICollectionViewFlowLayout库来做左,右或完全(基本上是默认的)水平对齐 - 它还允许您设置每个单元格之间的绝对距离。我也打算为它添加水平中心对齐和垂直对齐。

https://github.com/eroth/ERJustifiedFlowLayout

答案 6 :(得分:5)

迅速。根据迈克尔斯的回答

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
}

答案 7 :(得分:5)

以下是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
    }
}

异常:自动调整单元格

遗憾的是有一个大的例外。如果您使用UICollectionViewFlowLayout的{​​{1}}。内部estimatedItemSize正在改变一些事情。我没有完全跟踪它,但它在UICollectionViewFlowLayout自我调整单元格之后明确调用其他方法。根据我的反复试验,我发现在自动调整过程中,每个单元格似乎都会更频繁地调用layoutAttributesForElementsInRect。此更新layoutAttributesForItemAtIndexPath适用于LeftAlignedFlowLayout。它也适用于静态大小的单元格,但是额外的布局调用会让我在不需要自动调整单元格的情况下使用原始答案。

estimatedItemSize

答案 8 :(得分:5)

2019年的简单解决方案

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

基本上,您只是这样做:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // 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中就可以了。

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

这就是全部:

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首先澄清了这一点!

答案 9 :(得分:5)

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

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

答案 10 :(得分:3)

基于所有答案,我改变了一点,它对我有用

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
}

答案 11 :(得分:1)

如果您的最低部署目标是iOS 13,我强烈建议您利用组成布局(文档here,WWDC演示文稿here)。

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

让我给你看一个例子。假设我们需要显示一个包含关键字气泡列表的页面。

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

如您所见,这非常简单。

答案 12 :(得分:1)

如果您遇到任何问题-集合视图右侧的某些单元格超出了集合视图的范围。 然后请使用它-

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

使用 INT 代替比较 CGFloat 值。

答案 13 :(得分:1)

感谢Michael Sand's answer。我将它修改为多行的解决方案(每行的相同对齐顶部y),即左对齐,甚至是每个项目的间距。

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

答案 14 :(得分:1)

这是我寻找适用于Swift 5的最佳代码的旅程。我加入了该线程和其他一些线程的答案,以解决我遇到的警告和问题。在集合视图中滚动时,我有一个警告和一些异常行为。控制台将打印以下内容:

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

我遇到的另一个问题是,一些冗长的单元格在屏幕的右侧被裁剪了。另外,我在委托函数中设置了部分插图和minimumInteritemSpacing,这导致值未反映在自定义类中。解决该问题的方法是,将这些属性设置为布局的实例,然后再将其应用于我的集合视图。

这是我在集合视图中使用布局的方式:

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

答案 15 :(得分: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;
}

这是最终的 collectionview(具有自定义单元格宽度)最终的样子:collectionview

答案 16 :(得分:0)

基于所有答案。 适用于leftToRight和rightToLeft

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

答案 17 :(得分:0)

UICollectionView的问题在于它尝试自动调整可用区域中的单元格。 我这样做是首先定义行数和列数,然后定义该行和列的单元格大小

1)定义我的UICollectionView的部分(行):

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2)定义部分中的项目数。您可以为每个部分定义不同数量的项目。您可以使用'部分'获取部分编号。参数。

(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

注意:     在UICollectionView

Section == Row
IndexPath.Row == Column

答案 18 :(得分:0)

根据此处的答案,但在收集视图也有页眉或页脚时修复了崩溃和对齐问题。左对齐单元格对齐:

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

答案 19 :(得分:0)

以上代码适合我。我想分享相应的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
    }
}

答案 20 :(得分:0)

编辑AngelGarcíaOlloqui的回答是尊重minimumInteritemSpacing代表的collectionView(_:layout:minimumInteritemSpacingForSectionAt:),如果它实现的话。

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
}

答案 21 :(得分:0)

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