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

98

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

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

Я полагаю, мне нужно создать подкласс UICollectionViewFlowLayout, однако после прочтения некоторых руководств и т. Д. В Интернете я просто не понимаю, как это работает.

Дэмиен
источник

Ответы:

176

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

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

Swift:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

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

            layoutAttribute.frame.origin.x = leftMargin

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

        return attributes
    }
}

Если вы хотите, чтобы дополнительные представления сохраняли свой размер, добавьте следующее в верхней части закрытия 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;
}
Анхель Г. Оллоки
источник
4
Благодарность! Я получал предупреждение, которое let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
устраняется
2
Для тех, кто, возможно, наткнулся на это в поисках версии с выравниванием по центру, как и я, я разместил измененную версию ответа Ангела здесь: stackoverflow.com/a/38254368/2845237
Alex Koshy
1
Пробовал это решение, но не смог связать макет с collectionView на панели Interface Builder Utilies. Закончил этим кодом. Любая подсказка почему?
acrespo
1
Как добиться выравнивания по левому краю в системах с письмом слева направо и по правому краю в системах с письмом справа налево?
Рик
1
Для использования сделайте это self.collectionView.collectionViewLayout = LeftAlignedCollectionViewFlowLayout ()
blackops
64

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

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

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

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

Другими словами:

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


AlignedCollectionViewFlowLayout

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

⬅︎ слева :

Макет с выравниванием по левому краю

или ➡︎ правильно :

Макет с выравниванием по правому краю

и дополнительно предлагает варианты вертикального выравнивания ячеек в соответствующих строках (если они различаются по высоте).

Вы можете просто скачать его здесь:

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центры всех ячеек по вертикали, каждая ячейка в одной строке должна пересекаться с этим прямоугольником.

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

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

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

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


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

Миша
источник
Я применяю этот сценарий stackoverflow.com/questions/56349052/… но он не работает. не могли бы вы мне сказать, как я могу этого добиться?
Назмул Хасан
Даже с этим (это действительно потрясающий проект) я испытываю мигание в моих приложениях с вертикальной прокруткой коллекции. У меня 20 ячеек, и на первом свитке первая ячейка пуста, и все ячейки сдвинуты на одну позицию вправо. После одной полной прокрутки вверх и вниз все выравнивается правильно.
Парт Тамане
Шикарный проект! Было бы здорово, если бы он был включен в Карфаген.
pjuzeliunas
30

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


№1. Выровнять по левому краю автоизменение размера UICollectionViewCells

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

}

Ожидаемый результат:

введите описание изображения здесь


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

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

}

Ожидаемый результат:

введите описание изображения здесь

Иману Пети
источник
При группировании атрибутов ячеек по строкам используется 10ли произвольное число?
amariduran
27

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

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

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

    // 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 - готово.

Полное рабочее решение для копирования и вставки:

Вот и все:

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

введите описание изображения здесь

И наконец...

Поблагодарите @AlexShubin выше, который первым разъяснил это!

Толстяк
источник
Привет. Я немного запуталась. Добавление пробела для каждой строки в начале и конце по-прежнему показывает мое слово в ячейке элемента без добавленного места. Есть идеи, как это сделать?
Мохамед Ли
1
Если у вас есть заголовки разделов в вашей коллекции, это, кажется, их убивает.
TylerJames
22

Вопрос был задан некоторое время, но ответа нет, и это хороший вопрос. Ответ - переопределить один метод в подклассе 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 на ноль. Затем для первой ячейки и каждой ячейки вы добавляете ширину этой ячейки плюс некоторый запас. Это передается следующему элементу цикла. Если это не первый элемент, вы устанавливаете его origin.x на текущую вычисляемую маржу и добавляете новые элементы в массив.

Майк Сэнд
источник
Я думаю, что это намного лучше, чем решение Криса Вагнера ( stackoverflow.com/a/15554667/482557 ) по связанному вопросу - в вашей реализации нет повторяющихся вызовов UICollectionViewLayout.
Evan R
1
Не похоже, что это работает, если в строке есть один элемент, потому что UICollectionViewFlowLayout центрирует его. Вы можете обратиться в центр, чтобы решить проблему. Что-то вродеattribute.center.x == self.collectionView?.bounds.midX
Райан Пулос
Я реализовал это, но по какой-то причине он ломается для первого раздела: первая ячейка появляется слева от первой строки, а вторая ячейка (которая не помещается в той же строке) переходит в следующую строку, но не слева выровнен. Вместо этого его позиция x начинается там, где заканчивается первая ячейка.
Николас Миари
@NicolasMiari Цикл решает, что это новая строка, и сбрасывает ее leftMarginс if (attributes.frame.origin.x == self.sectionInset.left) {проверкой. Это предполагает, что макет по умолчанию выровнял ячейку новой строки так, чтобы она была на одном уровне с sectionInset.left. Если это не так, результатом будет описанное вами поведение. Я бы вставил точку останова внутри цикла и сразу перед сравнением проверил бы ячейку второй строки с обеих сторон. Удачи.
Майк Сэнд,
3
Спасибо ... В итоге я использовал UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Он отменяет еще пару методов.
Николас Миари,
9

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

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
мклб
источник
Отсутствует открывающая квадратная скобка в вашей инициализации макета
RyanOfCourse
Я проверил два ответа: github.com/eroth/ERJustifiedFlowLayout и UICollectionViewLeftAlignedLayout. Только второй результат дал правильный результат при использовании EstimatedItemSize (самоизмерения).
EckhardN
6

Вот оригинальный ответ на 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«S 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
    }
}
Райан Пулос
источник
Это работает для первого раздела, однако размер метки во втором разделе не изменяется, пока таблица не прокручивается. Мое представление коллекции находится в ячейке таблицы.
EI Captain v2.0
5

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

https://github.com/eroth/ERJustifiedFlowLayout

Эван Р
источник
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
}
GregP
источник
4

Основываясь на всех ответах, я немного меняю, и мне это нравится

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
}
Котваска
источник
4

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

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 .

нику
источник
3

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

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

        var leftMargin = sectionInset.left
        var prevMaxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in

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

            if layoutAttribute.frame.origin.y >= prevMaxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            prevMaxY = layoutAttribute.frame.maxY
        }

        return attributes
    }
}
Алексей Шубин
источник
2

Спасибо за ответ Майкла Сэнда . Я изменил его на решение из нескольких строк (одинаковое выравнивание по верху 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;
}
Шу Чжан
источник
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
    }
}
Youstanzr
источник
Ты спас мне день
Дэвид 'mArm' Ансермот
0

Проблема с 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
Анас Икбал
источник
0

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

#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;
}
Лал Кришна
источник
0

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