Выравнивание по левому краю ячеек в UICollectionView

Я использую UICollectionView в моем проекте, где есть несколько ячеек различной ширины на линии. В соответствии с: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

он распределяет ячейки по всей линии с равным заполнением. Это происходит, как и ожидалось, за исключением того, что я хочу, чтобы их оправдали, и жесткий код ширины заполнения.

Я полагаю, что мне нужно подклассифицировать UICollectionViewFlowLayout, однако после прочтения некоторых обучающих программ и т.д. онлайн я просто не понимаю, как это работает.

Ответ 1

Другие решения здесь не работают должным образом, когда строка состоит только из 1 элемента или слишком сложна.

Основываясь на примере, приведенном Райаном, я изменил код, чтобы обнаружить новую строку, проверив позицию 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
}

Цель-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;
}

Ответ 2

Есть много прекрасных идей, включенных в ответы на этот вопрос. Однако большинство из них имеют некоторые недостатки:

  • Решения, которые не проверяют значение y в ячейке , работают только для однострочных макетов. Они не подходят для макетов представления коллекции с несколькими строками.
  • Решения, которые проверяют значение y, например Angel García Olloqui answer ,работают, только если все ячейки имеют одинаковую высоту. Они не подходят для ячеек с переменной высотой.
  • Большинство решений перекрывают только функцию layoutAttributesForElements(in rect: CGRect). Они не переопределяют layoutAttributesForItem(at indexPath: IndexPath). Это проблема, потому что представление коллекции периодически вызывает последнюю функцию для получения атрибутов макета для определенного пути индекса. Если вы не вернете надлежащие атрибуты из этой функции, вы, скорее всего, столкнетесь со всевозможными визуальными ошибками, например, во время вставки и удаления анимаций ячеек или при использовании ячеек с саморазмерением путем настройки макета представления коллекции estimatedItemSize. Apple Docs состояние:

    Предполагается, что каждый объект пользовательского макета реализует метод layoutAttributesForItemAtIndexPath:.

  • Многие решения также делают предположения о параметре rect, который передается в функцию layoutAttributesForElements(in rect: CGRect). Например, многие основаны на предположении, что rect всегда начинается с начала новой строки, что не всегда так.

Итак, другими словами:

Большинство решений, предлагаемых на этой странице, работают для некоторых конкретных приложений, но они не работают должным образом в каждой ситуации.


AlignedCollectionViewFlowLayout

Для решения этих проблем я создал подкласс UICollectionViewFlowLayout, который следует аналогичной идее, предложенной matt и Крисом Вагнером в их ответах на аналогичный вопрос. Можно либо выровнять ячейки

⬅︎ осталось:

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() просто создает полную копию всех атрибутов макета в массиве. Вы можете посмотреть исходный код для его реализации.)

Так что теперь единственное, что нам нужно сделать, - это правильно реализовать функцию layoutAttributesForItem(at indexPath: IndexPath). Суперкласс UICollectionViewFlowLayout уже помещает правильное количество ячеек в каждой строке, поэтому нам нужно только сместить их влево в пределах их соответствующего ряда. Сложность заключается в том, чтобы вычислить объем пространства, необходимого для смещения каждой ячейки влево.

Поскольку мы хотим иметь фиксированный интервал между ячейками, основная идея состоит в том, чтобы просто предположить, что предыдущая ячейка (ячейка слева от ячейки, которая в данный момент расположена) уже правильно размещена. Затем нам нужно только добавить расстояние между ячейками к значению maxX предыдущего кадра ячейки и значение origin.x для текущего кадра ячейки.

Теперь нам нужно знать только, когда мы достигли начала строки, чтобы мы не выравнивали ячейку рядом с ячейкой в предыдущей строке. (Это приведет не только к неправильной компоновке, но и к запаздыванию.) Так что нам нужно иметь привязку рекурсии. Подход, который я использую для нахождения этой рекурсивной привязки, следующий:

Чтобы узнать, находится ли ячейка с индексом i в той же строке, что и ячейка с индексом i-1...

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

... Я "рисую" прямоangularьник вокруг текущей ячейки и растягиваю его по ширине всего представления коллекции. Поскольку UICollectionViewFlowLayout центрирует все ячейки по вертикали, каждая ячейка в одной строке должна пересекаться с этим прямоangularьником.

Таким образом, я просто проверяю, пересекается ли ячейка с индексом i-1 с прямоangularьником этой линии, созданным из ячейки с индексом i.

  • Если оно пересекается, ячейка с индексом i не является самой левой ячейкой в строке.
    → Получить предыдущий кадр ячейки (с индексом i-1) и переместить текущую ячейку рядом с ним.

  • Если он не пересекается, ячейка с индексом i является самой левой ячейкой в строке.
    → Переместите ячейку к левому краю представления коллекции (без изменения ее вертикального положения).

Я не буду публиковать здесь фактическую реализацию функции layoutAttributesForItem(at indexPath: IndexPath), потому что я думаю, что наиболее важной частью является понимание идеи, и вы всегда можете проверить мою реализацию в исходном коде. (Это немного сложнее, чем объяснено здесь, потому что я также допускаю выравнивание .right и различные варианты вертикального выравнивания. Однако, оно следует той же идее.)


Ух ты, я думаю, это самый длинный ответ, который я когда-либо писал на Stackoverflow. Надеюсь, это поможет. 😉

Ответ 3

В Swift 4.1 и iOS 11, в соответствии с вашими потребностями, вы можете выбрать одну из 2 следующих полных реализаций, чтобы решить вашу проблему.


# 1. Автоматическое изменение размера по левому краю UICollectionViewCell s

Реализация ниже показывает, как использовать UICollectionViewLayout layoutAttributesForElements(in:), UICollectionViewFlowLayout estimatedItemSize и UILabel preferredMaxLayoutWidth для выравнивания по левому краю авторазмер ячеек в 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 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


# 2. Выровнять по левому краю UICollectionViewCell с фиксированным размером

В приведенной ниже реализации показано, как использовать UICollectionViewLayout layoutAttributesForElements(in:) и UICollectionViewFlowLayout itemSize для выравнивания ячеек влево с предварительно определенным размером в 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 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

Ответ 4

Вопрос встал, но ответа нет, и это хороший вопрос. Ответ заключается в переопределении одного метода в подклассе 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

Как рекомендовано Apple, вы получаете атрибуты макета от супер и итерации по ним. Если он первый в строке (определяется его origin.x, находящимся на левом поле), вы оставите его в покое и reset x равным нулю. Затем для первой ячейки и каждой ячейки вы добавляете ширину этой ячейки плюс некоторый запас. Это передается в следующий элемент цикла. Если это не первый элемент, вы устанавливаете его origin.x в текущую расчетную маржу и добавляете новые элементы в массив.

Ответ 5

У меня была та же проблема, Попробуйте Cocoapod UICollectionViewLeftAlignedLayout. Просто включите его в свой проект и выполните его инициализацию следующим образом:

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

Ответ 6

Основываясь на Ответ Майкла Санда, я создал подклассовую библиотеку UICollectionViewFlowLayout для выполнения горизонтального выравнивания по левому, правому или полному (в основном по умолчанию) - это также позволяет вы устанавливаете абсолютное расстояние между каждой ячейкой. Я планирую добавить к нему горизонтальное выравнивание по центру и вертикальное обоснование.

https://github.com/eroth/ERJustifiedFlowLayout

Ответ 7

Быстро. По словам Майклса, ответьте

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
}

Ответ 8

Вот оригинальный ответ в 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 estimatedItemSize. Внутренне UICollectionViewFlowLayout немного меняет ситуацию. Я не отслеживал его полностью, но его ясность вызывала другие методы после layoutAttributesForElementsInRect, в то время как собственные размеры ячеек. Из моей пробной версии и ошибки я обнаружил, что для каждой ячейки, по-видимому, чаще всего называют layoutAttributesForItemAtIndexPath для каждой ячейки. Этот обновленный LeftAlignedFlowLayout отлично работает с estimatedItemSize. Он также работает со статическими ячейками, однако дополнительные вызовы макета позволяют мне использовать исходный ответ в любое время, когда мне не нужны автосоздания ячеек.

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

Ответ 9

Основываясь на всех ответах, я немного меняюсь, и это работает хорошо для меня.

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
}

Ответ 10

На основе ответов здесь, но исправлены сбои и проблемы с выравниванием, когда в вашем представлении коллекции также есть верхние или нижние колонтитулы. Выравнивание только оставшихся ячеек:

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

Ответ 11

Спасибо за ответ Майкл Санд. Я изменил его на решение нескольких строк (такое же выравнивание Top 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;
}

Ответ 12

Если у кого-то из вас возникла проблема - некоторые из ячеек, которые в правой части представления коллекции превышают границы представления коллекции. Тогда, пожалуйста, используйте это -

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

Проблема с UICollectionView заключается в том, что он пытается автоматически поместить ячейки в доступную область. Я сделал это, сначала определяя количество строк и столбцов, а затем определяя размер ячейки для этой строки и столбца

1) Чтобы определить разделы (строки) моего 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

Примечание:   В UICollectionView

Section == Row
IndexPath.Row == Column

Ответ 14

Ответ Майка Санда хорош, но у меня возникли некоторые проблемы с этим кодом (например, длинные ячейки вырезаны). И новый код:

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

Ответ 15

Отредактированный Ангел Гарсия Оллоки отвечает на уважение 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
}

Ответ 16

Этот код работает для меня. Я хотел бы поделиться соответствующим кодом 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
    }
}

Ответ 17

Простое решение в 2019 году

Это один из тех удручающих вопросов, когда за последние годы многое изменилось. Теперь это легко.

В основном вы просто делаете это:

    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 выше, который первым разъяснил это!