Alinhar células à esquerda em UICollectionView

95

Estou usando um UICollectionView em meu projeto, onde existem várias células de larguras diferentes em uma linha. De acordo com: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

ele espalha as células ao longo da linha com preenchimento igual. Isso acontece como esperado, exceto que eu quero justificá-los à esquerda e codificar uma largura de preenchimento.

Acho que preciso criar uma subclasse de UICollectionViewFlowLayout, no entanto, depois de ler alguns dos tutoriais etc. online, simplesmente não consigo entender como isso funciona.

Damien
fonte

Respostas:

169

As outras soluções neste segmento não funcionam bem, quando a linha é composta por apenas 1 item ou são muito complicadas.

Com base no exemplo fornecido por Ryan, alterei o código para detectar uma nova linha inspecionando a posição Y do novo elemento. Muito simples e rápido no desempenho.

Rápido:

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

Se você quiser que as visualizações suplementares mantenham seu tamanho, adicione o seguinte na parte superior do encerramento na forEachchamada:

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;
}
Angel G. Olloqui
fonte
4
Obrigado! Estava recebendo um aviso, que foi corrigido ao fazer uma cópia dos atributos na matriz let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou
2
Para qualquer um que possa ter tropeçado procurando por uma versão alinhada ao centro como eu fiz, postei uma versão modificada da resposta de Angel aqui: stackoverflow.com/a/38254368/2845237
Alex Koshy
Tentei esta solução, mas algumas das células que estão à direita da visualização da coleção excedem os limites da visualização da coleção. Alguém mais encontrou isso?
Happiehappie
@Happiehappie sim, eu também tenho esse comportamento. alguma correção?
John Baum
Tentei essa solução, mas não consegui associar o layout a collectionView no painel Interface Builder Utilies. Acabou fazendo isso por código. Alguma pista do porquê?
acrespo de
61

Existem muitas ideias excelentes incluídas nas respostas a esta pergunta. No entanto, a maioria deles tem algumas desvantagens:

  • Soluções que não verificam o valor y da célula funcionam apenas para layouts de linha única . Eles falham para layouts de visualização de coleção com várias linhas.
  • Soluções que fazer verificar o y valor como resposta de Angel García Olloqui único trabalho se todas as células têm a mesma altura . Eles falham para células com altura variável.
  • A maioria das soluções apenas substitui a layoutAttributesForElements(in rect: CGRect)função. Eles não se sobrepõem layoutAttributesForItem(at indexPath: IndexPath). Isso é um problema porque a visualização de coleção chama periodicamente a última função para recuperar os atributos de layout para um caminho de índice específico. Se você não retornar os atributos apropriados dessa função, é provável que você encontre todos os tipos de bugs visuais, por exemplo, durante a inserção e exclusão de animações de células ou ao usar células autodimensionadas definindo o layout de visualização da coleção estimatedItemSize. Os documentos da Apple afirmam:

    Cada objeto de layout personalizado deve implementar o layoutAttributesForItemAtIndexPath:método.

  • Muitas soluções também fazem suposições sobre o rectparâmetro que é passado para a layoutAttributesForElements(in rect: CGRect)função. Por exemplo, muitos se baseiam na suposição de que rectsempre começa no início de uma nova linha, o que não é necessariamente o caso.

Em outras palavras:

A maioria das soluções sugeridas nesta página funcionam para alguns aplicativos específicos, mas não funcionam como esperado em todas as situações.


AlignedCollectionViewFlowLayout

Para resolver esses problemas, criei uma UICollectionViewFlowLayoutsubclasse que segue uma ideia semelhante à sugerida por matt e Chris Wagner em suas respostas a uma pergunta semelhante. Ele pode alinhar as células

⬅︎ esquerda :

Layout alinhado à esquerda

ou ➡︎ certo :

Layout alinhado à direita

e, adicionalmente, oferece opções para alinhar verticalmente as células em suas respectivas linhas (no caso de variarem em altura).

Você pode simplesmente fazer o download aqui:

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

O uso é direto e explicado no arquivo README. Basicamente, você cria uma instância de AlignedCollectionViewFlowLayout, especifica o alinhamento desejado e o atribui à collectionViewLayoutpropriedade da visualização de sua coleção :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Também está disponível no Cocoapods .)


Como funciona (para células alinhadas à esquerda):

O conceito aqui é confiar exclusivamente na layoutAttributesForItem(at indexPath: IndexPath)função . No layoutAttributesForElements(in rect: CGRect), simplesmente obtemos os caminhos do índice de todas as células dentro do recte, em seguida, chamamos a primeira função para cada caminho do índice para recuperar os quadros corretos:

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
}

(A copy()função simplesmente cria uma cópia profunda de todos os atributos de layout na matriz. Você pode examinar o código-fonte para sua implementação.)

Portanto, agora a única coisa que temos que fazer é implementar a layoutAttributesForItem(at indexPath: IndexPath)função corretamente. A superclasse UICollectionViewFlowLayoutjá coloca o número correto de células em cada linha, portanto, só temos que deslocá-las para a esquerda em suas respectivas linhas. A dificuldade está em calcular a quantidade de espaço de que precisamos para deslocar cada célula para a esquerda.

Como queremos um espaçamento fixo entre as células, a ideia central é apenas assumir que a célula anterior (a célula à esquerda da célula que está atualmente disposta) já está posicionada corretamente. Então, só temos que adicionar o espaçamento da célula ao maxXvalor do quadro da célula anterior e esse é oorigin.x valor do quadro da célula atual.

Agora só precisamos saber quando alcançamos o início de uma linha, para não alinharmos uma célula ao lado de uma célula na linha anterior. (Isso não resultaria apenas em um layout incorreto, mas também seria extremamente lento.) Portanto, precisamos ter uma âncora de recursão. A abordagem que uso para encontrar essa âncora de recursão é a seguinte:

Para descobrir se a célula no índice i está na mesma linha que a célula com índice i-1 ...

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

... Eu "desenho" um retângulo ao redor da célula atual e o estico ao longo de toda a visualização da coleção. Como o UICollectionViewFlowLayoutcentro de todas as células verticalmente, todas as células da mesma linha devem cruzar com este retângulo.

Assim, eu simplesmente verifico se a célula com índice i-1 se cruza com este retângulo de linha criado a partir da célula com índice i .

  • Se houver interseção, a célula com índice i não é a célula mais à esquerda da linha.
    → Obtenha o quadro da célula anterior (com o índice i − 1 ) e mova a célula atual para perto dela.

  • Se não houver interseção, a célula com índice i é a célula mais à esquerda da linha.
    → Mova a célula para a borda esquerda da visualização da coleção (sem alterar sua posição vertical).

Não vou postar a implementação real da layoutAttributesForItem(at indexPath: IndexPath)função aqui porque acho que a parte mais importante é entender a ideia e você sempre pode verificar minha implementação no código-fonte . (É um pouco mais complicado do que explicado aqui porque também permito o .rightalinhamento e várias opções de alinhamento vertical. No entanto, segue a mesma ideia.)


Uau, acho que esta é a resposta mais longa que já escrevi no Stackoverflow. Eu espero que isso ajude. 😉

Mischa
fonte
Eu aplico com este cenário stackoverflow.com/questions/56349052/… mas não está funcionando. você poderia me dizer como posso conseguir isso
Nazmul Hasan
Mesmo com isso (que é um projeto realmente incrível), eu experimento piscar na visualização da coleção de rolagem vertical do meu aplicativo. Tenho 20 células e, no primeiro pergaminho, a primeira célula está em branco e todas as células são deslocadas uma posição para a direita. Depois de uma rolagem completa para cima e para baixo, as coisas se alinham corretamente.
Parth Tamane 01 de
Projeto incrível! Seria ótimo vê-lo habilitado para Cartago.
pjuzeliunas
30

Com o Swift 4.1 e iOS 11, de acordo com as suas necessidades, você pode escolher uma das 2 implementações completas a seguir para corrigir o seu problema.


# 1. Deixou o redimensionamento automático alinhar UICollectionViewCells

A implementação abaixo mostra como usar UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSizee UILabel's preferredMaxLayoutWidthpara alinhar à esquerda as células de redimensionamento automático em 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")
    }

}

Resultado esperado:

insira a descrição da imagem aqui


# 2. Alinhamento à esquerda UICollectionViewCellcom tamanho fixo

A implementação abaixo mostra como usar UICollectionViewLayout's layoutAttributesForElements(in:)e UICollectionViewFlowLayout' s itemSize, a fim de células alinhadas à esquerda com tamanho pré-definido em um 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")
    }

}

Resultado esperado:

insira a descrição da imagem aqui

Imanou Petit
fonte
Ao agrupar os atributos da célula por linha, é o uso de 10um número arbitrário?
amariduran
25

A solução simples em 2019

Esta é uma daquelas questões deprimentes em que as coisas mudaram muito ao longo dos anos. Agora é fácil.

Basicamente, você apenas faz isso:

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

Tudo que você precisa agora é o código padrão:

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
}

Basta copiar e colar em um UICollectionViewFlowLayout - pronto.

Solução de trabalho completa para copiar e colar:

Isso é tudo:

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

insira a descrição da imagem aqui

E finalmente...

Agradeça a @AlexShubin acima, que primeiro esclareceu isso!

Fattie
fonte
Ei. Eu estou um pouco confuso. Adicionar espaço para cada string no início e no final ainda mostra minha palavra na célula do item sem o espaço que adicionei. Alguma ideia de como fazer isso?
Mohamed Lee
Se você tiver cabeçalhos de seção em sua visualização de coleção, isso parece matá-los.
TylerJames
22

A pergunta já se levantou há algum tempo, mas não há resposta e é uma boa pergunta. A resposta é substituir um método na subclasse 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

Conforme recomendado pela Apple, você obtém os atributos de layout de super e itera sobre eles. Se for o primeiro na linha (definido por seu origin.x estar na margem esquerda), você o deixa sozinho e zera o x. Então, para a primeira célula e cada célula, você adiciona a largura dessa célula mais alguma margem. Isso é passado para o próximo item do loop. Se não for o primeiro item, você define origin.x para a margem calculada em execução e adiciona novos elementos ao array.

Mike Sand
fonte
Acho que isso é muito melhor do que a solução de Chris Wagner ( stackoverflow.com/a/15554667/482557 ) na questão vinculada - sua implementação tem zero chamadas UICollectionViewLayout repetitivas.
Evan R
1
Não parece que isso funciona se a linha tiver um único item porque UICollectionViewFlowLayout o centraliza. Você pode verificar o centro para resolver o problema. Algo comoattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos
Eu implementei isso, mas por algum motivo ele quebra na primeira seção: a primeira célula aparece à esquerda da primeira linha, e a segunda célula (que não cabe na mesma linha) vai para a próxima linha, mas não à esquerda alinhado. Em vez disso, sua posição x começa onde termina a primeira célula.
Nicolas Miari
@NicolasMiari O loop decide que é uma nova linha e redefine o leftMargincom a if (attributes.frame.origin.x == self.sectionInset.left) {marca de seleção. Isso pressupõe que o layout padrão alinhou a célula da nova linha para ficar alinhada com o sectionInset.left. Se não for esse o caso, o comportamento que você descreve seria o resultado. Gostaria de inserir um ponto de interrupção dentro do loop e verificar os dois lados para a célula da segunda linha antes da comparação. Boa sorte.
Mike Sand,
3
Obrigado ... Acabei usando UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Ele substitui mais alguns métodos.
Nicolas Miari
9

Eu tive o mesmo problema, experimente o Cocoapod UICollectionViewLeftAlignedLayout . Basta incluí-lo em seu projeto e inicializá-lo assim:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
mklb
fonte
Falta um colchete de abertura na inicialização do layout
RyanOfCourse
Testei as duas respostas github.com/eroth/ERJustifiedFlowLayout e UICollectionViewLeftAlignedLayout. Somente o segundo produziu o resultado correto ao usar estimadoItemSize (dimensionamento automático).
EckhardN
5

Com base na resposta de Michael Sand , criei uma UICollectionViewFlowLayoutbiblioteca com subclasses para fazer a justificação horizontal à esquerda, à direita ou completa (basicamente o padrão) - ela também permite definir a distância absoluta entre cada célula. Pretendo adicionar a justificação central horizontal e a justificação vertical também.

https://github.com/eroth/ERJustifiedFlowLayout

Evan R
fonte
5

Aqui está a resposta original em Swift. Ainda funciona muito bem na maioria das vezes.

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

Exceção: Dimensionamento Automático de Células

Infelizmente, há uma grande exceção. Se você estiver usando UICollectionViewFlowLayouto estimatedItemSize. Internamente UICollectionViewFlowLayoutestá mudando um pouco as coisas. Eu não o rastreei totalmente, mas é claro que ele chama outros métodos após o layoutAttributesForElementsInRectauto-dimensionamento de células. Pela minha tentativa e erro, descobri que parece exigir layoutAttributesForItemAtIndexPathcada célula individualmente durante o dimensionamento automático com mais frequência. Esta atualização LeftAlignedFlowLayoutfunciona muito bem com estimatedItemSize. Ele funciona com células de tamanho estático também, no entanto, as chamadas de layout extras me levam a usar a resposta original sempre que não precisar de dimensionamento automático de células.

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
    }
}
Ryan Poolos
fonte
Isso funciona para a primeira seção, entretanto o rótulo não é redimensionado na segunda seção até que a tabela role. Minha visão de coleção está na célula da tabela.
Capitão EI v2.0
5

Rápido. De acordo com a resposta de Michaels

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
fonte
4

Com base em todas as respostas, eu mudo um pouco e funciona bem para mim

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
}
Kotvaska
fonte
4

Se algum de vocês estiver enfrentando problemas - algumas das células que estão à direita da visualização da coleção excedendo os limites da visualização da coleção . Então, por favor, use este -

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

Use INT em vez de comparar os valores CGFloat .

Niku
fonte
3

Com base nas respostas aqui, mas foram corrigidos travamentos e problemas de alinhamento quando sua visualização de coleção também tem cabeçalhos ou rodapés. Alinhando apenas células à esquerda:

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
    }
}
Alex Shubin
fonte
2

Obrigado pela resposta do Michael Sand . Eu modifiquei para uma solução de várias linhas (o mesmo alinhamento Top y de cada linha) que é Alinhamento à Esquerda, com espaçamento uniforme para cada item.

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;
}
Shu Zhang
fonte
1

Aqui está minha jornada para encontrar o melhor código que funcione com o Swift 5. Eu juntei algumas respostas deste tópico e alguns outros tópicos para resolver avisos e problemas que enfrentei. Tive um aviso e um comportamento anormal ao rolar pela visualização da minha coleção. O console imprime o seguinte:

Isso provavelmente está ocorrendo porque o layout de fluxo "xyz" está modificando os atributos retornados por UICollectionViewFlowLayout sem copiá-los.

Outro problema que enfrentei é que algumas células longas estão sendo cortadas no lado direito da tela. Além disso, eu estava definindo as inserções de seção e o minimumInteritemSpacing nas funções de delegado que resultaram em valores não refletidos na classe personalizada. A correção para isso foi definir esses atributos para uma instância do layout antes de aplicá-lo à visualização da minha coleção.

Veja como usei o layout para a visualização da minha coleção:

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)

Aqui está a classe de layout de fluxo

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
fonte
Você salvou meu dia
David 'mArm' Ansermot
0

O problema com UICollectionView é que ele tenta ajustar automaticamente as células na área disponível. Eu fiz isso definindo primeiro o número de linhas e colunas e, em seguida, definindo o tamanho da célula para essa linha e coluna

1) Para definir seções (linhas) de meu UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Para definir o número de itens em uma seção. Você pode definir um número diferente de itens para cada seção. você pode obter o número da seção usando o parâmetro 'seção'.

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

3) Para definir o tamanho da célula para cada seção e linha separadamente. Você pode obter o número da seção e o número da linha usando o parâmetro 'indexPath', ou seja, [indexPath section]para o número da seção e [indexPath row]para o número da linha.

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

4) Em seguida, você pode exibir suas células em linhas e seções usando:

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

NOTA: Em UICollectionView

Section == Row
IndexPath.Row == Column
Anas iqbal
fonte
0

A resposta de Mike Sand é boa, mas tive alguns problemas com este código (como uma célula longa cortada). E novo código:

#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;
}
Lal Krishna
fonte
0

Editou a resposta de Angel García Olloqui ao respeito minimumInteritemSpacingdo delegado collectionView(_:layout:minimumInteritemSpacingForSectionAt:), se a implementa.

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
}
Radek
fonte
0

O código acima funciona para mim. Eu gostaria de compartilhar o respectivo código 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
    }
}
Naresh
fonte