Aligner à gauche les cellules dans UICollectionView

96

J'utilise un UICollectionView dans mon projet, où il y a plusieurs cellules de largeurs différentes sur une ligne. Selon: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

il répartit les cellules sur la ligne avec un remplissage égal. Cela se produit comme prévu, sauf que je veux les justifier à gauche et coder en dur une largeur de remplissage.

Je pense que je dois sous-classer UICollectionViewFlowLayout, mais après avoir lu certains des didacticiels, etc. en ligne, je ne semble tout simplement pas comprendre comment cela fonctionne.

Damien
la source
Reproduction
Cœur

Réponses:

169

Les autres solutions de ce fil ne fonctionnent pas correctement, lorsque la ligne est composée d'un seul élément ou sont trop compliquées.

Sur la base de l'exemple donné par Ryan, j'ai changé le code pour détecter une nouvelle ligne en inspectant la position Y du nouvel élément. Très simple et rapide en performance.

Rapide:

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

Si vous souhaitez que les vues supplémentaires conservent leur taille, ajoutez ce qui suit en haut de la clôture de l' forEachappel:

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

Objectif 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
la source
4
Merci! Je recevais un avertissement, qui est corrigé en faisant une copie des attributs dans le tableau let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou
2
Pour tous ceux qui sont peut-être tombés sur cela à la recherche d'une version alignée au centre comme je l'ai fait, j'ai publié une version modifiée de la réponse d'Angel ici: stackoverflow.com/a/38254368/2845237
Alex Koshy
J'ai essayé cette solution, mais une partie de la cellule qui se trouve à droite de la vue de collection dépasse les limites de la vue de collection. Quelqu'un d'autre a-t-il rencontré cela?
Happiehappie
@Happiehappie oui j'ai ce comportement aussi. une solution?
John Baum du
J'ai essayé cette solution, mais je n'ai pas pu associer la mise en page à collectionView dans le panneau Interface Builder Utilies. J'ai fini par le faire par code. Une idée pourquoi?
acrespo
63

Les réponses à cette question contiennent de nombreuses idées intéressantes. Cependant, la plupart d'entre eux présentent certains inconvénients:

  • Les solutions qui ne vérifient pas la valeur y de la cellule ne fonctionnent que pour les mises en page sur une seule ligne . Ils échouent pour les mises en page de vue de collection avec plusieurs lignes.
  • Des solutions qui font vérifier la y valeur comme réponse Angel García Olloqui que le travail si toutes les cellules ont la même hauteur . Ils échouent pour les cellules de hauteur variable.
  • La plupart des solutions remplacent uniquement la layoutAttributesForElements(in rect: CGRect)fonction. Ils ne prévalent pas layoutAttributesForItem(at indexPath: IndexPath). Ceci est un problème car la vue de collection appelle périodiquement la dernière fonction pour récupérer les attributs de mise en page pour un chemin d'index particulier. Si vous ne renvoyez pas les attributs appropriés de cette fonction, vous risquez de rencontrer toutes sortes de bogues visuels, par exemple lors des animations d'insertion et de suppression de cellules ou lors de l'utilisation de cellules auto-dimensionnées en définissant la disposition de la vue de collection estimatedItemSize. L' état des documents Apple :

    Chaque objet de mise en page personnalisé doit implémenter la layoutAttributesForItemAtIndexPath:méthode.

  • De nombreuses solutions émettent également des hypothèses sur le rectparamètre passé à la layoutAttributesForElements(in rect: CGRect)fonction. Par exemple, beaucoup sont basés sur l'hypothèse que le rectcommence toujours au début d'une nouvelle ligne, ce qui n'est pas nécessairement le cas.

Donc en d'autres termes:

La plupart des solutions suggérées sur cette page fonctionnent pour certaines applications spécifiques, mais elles ne fonctionnent pas comme prévu dans toutes les situations.


AlignedCollectionViewFlowLayout

Afin de résoudre ces problèmes, j'ai créé une UICollectionViewFlowLayoutsous-classe qui suit une idée similaire à celle suggérée par Matt et Chris Wagner dans leurs réponses à une question similaire. Il peut soit aligner les cellules

⬅︎ à gauche :

Disposition alignée à gauche

ou à ➡︎ droite :

Disposition alignée à droite

et offre en outre des options pour aligner verticalement les cellules dans leurs rangées respectives (au cas où elles varient en hauteur).

Vous pouvez simplement le télécharger ici:

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

L'utilisation est simple et expliquée dans le fichier README. En gros, vous créez une instance de AlignedCollectionViewFlowLayout, spécifiez l'alignement souhaité et attribuez-le à la collectionViewLayoutpropriété de votre vue de collection :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Il est également disponible sur Cocoapods .)


Comment ça marche (pour les cellules alignées à gauche):

Le concept ici est de s'appuyer uniquement sur la layoutAttributesForItem(at indexPath: IndexPath)fonction . Dans le, layoutAttributesForElements(in rect: CGRect)nous obtenons simplement les chemins d'index de toutes les cellules dans le rect, puis appelons la première fonction pour chaque chemin d'index pour récupérer les images correctes:

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
}

(La copy()fonction crée simplement une copie complète de tous les attributs de disposition du tableau. Vous pouvez consulter le code source pour son implémentation.)

Alors maintenant, la seule chose que nous avons à faire est d'implémenter layoutAttributesForItem(at indexPath: IndexPath)correctement la fonction. La super classe UICollectionViewFlowLayoutplace déjà le nombre correct de cellules dans chaque ligne, nous n'avons donc qu'à les déplacer vers la gauche dans leur ligne respective. La difficulté réside dans le calcul de la quantité d'espace dont nous avons besoin pour déplacer chaque cellule vers la gauche.

Comme nous voulons avoir un espacement fixe entre les cellules, l'idée de base est simplement de supposer que la cellule précédente (la cellule à gauche de la cellule actuellement disposée) est déjà positionnée correctement. Ensuite, il suffit d'ajouter l'espacement des cellules à la maxXvaleur du cadre de la cellule précédente et c'est la origin.xvaleur du cadre de la cellule actuelle.

Maintenant, nous avons seulement besoin de savoir quand nous avons atteint le début d'une ligne, afin de ne pas aligner une cellule à côté d'une cellule de la ligne précédente. (Cela entraînerait non seulement une mise en page incorrecte, mais ce serait également extrêmement lent.) Nous devons donc avoir une ancre de récursivité. L'approche que j'utilise pour trouver cette ancre de récursivité est la suivante:

Pour savoir si la cellule d'index i est dans la même ligne que la cellule d'index i-1 ...

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

... Je "dessine" un rectangle autour de la cellule courante et je l'étire sur toute la largeur de la vue de la collection. Comme les UICollectionViewFlowLayoutcentres toutes les cellules verticalement, chaque cellule de la même ligne doit croiser ce rectangle.

Ainsi, je vérifie simplement si la cellule d'index i-1 croise ce rectangle de ligne créé à partir de la cellule d'index i .

  • Si elle se coupe, la cellule d'index i n'est pas la cellule la plus à gauche de la ligne.
    → Récupère l'image de la cellule précédente (avec l'index i − 1 ) et déplace la cellule courante à côté d'elle.

  • Si elle ne se coupe pas, la cellule d'index i est la cellule la plus à gauche de la ligne.
    → Déplacez la cellule vers le bord gauche de la vue de collection (sans changer sa position verticale).

Je ne publierai pas l'implémentation réelle de la layoutAttributesForItem(at indexPath: IndexPath)fonction ici car je pense que la partie la plus importante est de comprendre l' idée et vous pouvez toujours vérifier mon implémentation dans le code source . (C'est un peu plus compliqué qu'expliqué ici car j'autorise également l' .rightalignement et diverses options d'alignement vertical. Cependant, cela suit la même idée.)


Wow, je suppose que c'est la réponse la plus longue que j'ai jamais écrite sur Stackoverflow. J'espère que ça aide. 😉

Mischa
la source
Je postule avec ce scénario stackoverflow.com/questions/56349052/… mais cela ne fonctionne pas. pourriez-vous me dire comment puis-je y parvenir
Nazmul Hasan
Même avec cela (qui est un projet vraiment génial), je vois le clignotement dans la vue de la collection à défilement vertical de mes applications. J'ai 20 cellules, et sur le premier défilement, la première cellule est vide et toutes les cellules sont décalées d'une position vers la droite. Après un défilement complet de haut en bas, les choses s'alignent correctement.
Parth Tamane
Projet génial! Ce serait formidable de le voir activé pour Carthage.
pjuzeliunas
30

Avec Swift 4.1 et iOS 11, selon vos besoins, vous pouvez choisir l'une des 2 implémentations complètes suivantes afin de résoudre votre problème.


#1. Alignement à gauche autoresizing UICollectionViewCells

La mise en œuvre ci - dessous montre comment utiliser UICollectionViewLayout« s layoutAttributesForElements(in:), UICollectionViewFlowLayout» s estimatedItemSizeet UILabel« s preferredMaxLayoutWidthpour les cellules gauche Autoresizing d'alignement dans 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")
    }

}

Résultat attendu:

entrez la description de l'image ici


# 2. Aligner à gauche UICollectionViewCellavec une taille fixe

La mise en œuvre ci-dessous montre comment utiliser UICollectionViewLayoutles layoutAttributesForElements(in:)et UICollectionViewFlowLayoutles itemSizepour aligner à gauche les cellules avec une taille prédéfinie dans un 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")
    }

}

Résultat attendu:

entrez la description de l'image ici

Imanou Petit
la source
Lors du regroupement des attributs de cellule par ligne, l'utilisation d' 10un nombre arbitraire est-elle?
amariduran
25

La solution simple en 2019

C'est l'une de ces questions déprimantes où les choses ont beaucoup changé au fil des ans. C'est maintenant facile.

En gros, vous faites simplement ceci:

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

Tout ce dont vous avez besoin maintenant est le code passe-partout:

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
}

Copiez et collez simplement cela dans un UICollectionViewFlowLayout- vous avez terminé.

Solution de travail complète pour copier et coller:

C'est le tout:

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

entrez la description de l'image ici

Et enfin...

Remerciez @AlexShubin ci-dessus qui a clarifié cela pour la première fois!

Fattie
la source
Hey. Je suis un peu confuse. L'ajout d'espace pour chaque chaîne au début et à la fin montre toujours mon mot dans la cellule d'élément sans l'espace que j'ai ajouté. Une idée de comment faire?
Mohamed Lee
Si vous avez des en-têtes de section dans votre vue de collection, cela semble les tuer.
TylerJames
22

La question se pose depuis un certain temps, mais il n'y a pas de réponse et c'est une bonne question. La réponse est de remplacer une méthode dans la sous-classe 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

Comme recommandé par Apple, vous obtenez les attributs de mise en page de super et les itérez dessus. Si c'est le premier de la ligne (défini par son origine.x étant sur la marge de gauche), vous le laissez seul et remettez le x à zéro. Ensuite, pour la première cellule et chaque cellule, vous ajoutez la largeur de cette cellule plus une marge. Cela est passé à l'élément suivant de la boucle. S'il ne s'agit pas du premier élément, définissez son origine.x sur la marge calculée en cours d'exécution et ajoutez de nouveaux éléments au tableau.

Mike Sand
la source
Je pense que c'est bien mieux que la solution de Chris Wagner ( stackoverflow.com/a/15554667/482557 ) sur la question liée - votre implémentation n'a aucun appel UICollectionViewLayout répétitif.
Evan R
1
Cela ne semble pas fonctionner si la ligne a un seul élément car UICollectionViewFlowLayout le centre. Vous pouvez consulter le centre pour résoudre le problème. Quelque chose commeattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos
J'ai mis en œuvre cela, mais pour une raison quelconque, cela se brise pour la première section: la première cellule apparaît à gauche de la première ligne et la deuxième cellule (qui ne tient pas sur la même ligne) va à la ligne suivante, mais pas à gauche aligné. Au lieu de cela, sa position x commence là où la première cellule se termine.
Nicolas Miari
@NicolasMiari La boucle décide qu'il s'agit d'une nouvelle ligne et réinitialise le leftMarginavec la if (attributes.frame.origin.x == self.sectionInset.left) {vérification. Cela suppose que la disposition par défaut a aligné la cellule de la nouvelle ligne pour qu'elle soit alignée avec le sectionInset.left. Si ce n'est pas le cas, le comportement que vous décrivez en résultera. J'insérerais un point d'arrêt à l'intérieur de la boucle et vérifierais les deux côtés pour la cellule de la deuxième ligne juste avant la comparaison. Bonne chance.
Mike Sand
3
Merci ... J'ai fini par utiliser UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Il remplace quelques méthodes supplémentaires.
Nicolas Miari
9

J'ai eu le même problème, essayez le Cocoapod UICollectionViewLeftAlignedLayout . Incluez-le simplement dans votre projet et initialisez-le comme ceci:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
mklb
la source
Il manque un crochet ouvrant dans votre initialisation de la mise en page
RyanOfCourse
J'ai testé les deux réponses github.com/eroth/ERJustifiedFlowLayout et UICollectionViewLeftAlignedLayout. Seul le second a produit le bon résultat lors de l'utilisation estiméeItemSize (auto-dimensionnement).
EckhardN
5

En me basant sur la réponse de Michael Sand , j'ai créé une UICollectionViewFlowLayoutbibliothèque sous- classée pour effectuer une justification horizontale à gauche, à droite ou complète (fondamentalement par défaut) - elle vous permet également de définir la distance absolue entre chaque cellule. Je prévois d'y ajouter une justification horizontale du centre et une justification verticale.

https://github.com/eroth/ERJustifiedFlowLayout

Evan R
la source
5

Voici la réponse originale dans Swift. Cela fonctionne toujours très bien.

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

Exception: redimensionnement automatique des cellules

Il y a malheureusement une grande exception. Si vous utilisez UICollectionViewFlowLayoutdes fichiers estimatedItemSize. En interne, UICollectionViewFlowLayoutcela change un peu les choses. Je ne l'ai pas entièrement retracé, mais il est clair qu'il appelle d'autres méthodes après avoir layoutAttributesForElementsInRectauto-dimensionné les cellules. D'après mes essais et erreurs, j'ai constaté qu'il semble appeler layoutAttributesForItemAtIndexPathplus souvent chaque cellule individuellement lors du dimensionnement automatique. Cette mise à jour LeftAlignedFlowLayoutfonctionne très bien avec estimatedItemSize. Cela fonctionne également avec des cellules de taille statique, mais les appels de mise en page supplémentaires m'amènent à utiliser la réponse d'origine chaque fois que je n'ai pas besoin de cellules de dimensionnement automatique.

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
la source
Cela fonctionne pour la première section, mais l'étiquette ne se redimensionne pas sur la deuxième section jusqu'à ce que le tableau défile. Ma vue de collection se trouve dans la cellule du tableau.
EI Captain v2.0
5

En rapide. Selon la réponse 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
la source
4

Sur la base de toutes les réponses, je change un peu et cela fonctionne bien pour moi

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
la source
4

Si l'un de vous est confronté à un problème, certaines des cellules qui se trouvent à droite de la vue de collection dépassent les limites de la vue de collection . Alors veuillez utiliser ceci -

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

Utilisez INT au lieu de comparer les valeurs CGFloat .

niku
la source
3

Basé sur les réponses ici, mais correction des plantages et des problèmes d'alignement lorsque la vue de votre collection contient également des en-têtes ou des pieds de page. Aligner les cellules à gauche uniquement:

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
la source
2

Merci pour la réponse de Michael Sand . Je l'ai modifié en une solution de plusieurs lignes (le même alignement en haut y de chaque ligne) qui est alignement à gauche, même espacement pour chaque élément.

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
la source
1

Voici mon parcours pour trouver le meilleur code qui fonctionne avec Swift 5. J'ai joint quelques réponses de ce fil et d'autres fils pour résoudre les avertissements et les problèmes auxquels j'ai été confronté. J'ai eu un avertissement et un comportement anormal lors du défilement dans la vue de ma collection. La console imprime les éléments suivants:

Cela se produit probablement parce que la disposition de flux "xyz" modifie les attributs renvoyés par UICollectionViewFlowLayout sans les copier.

Un autre problème auquel j'ai été confronté est que certaines cellules longues sont rognées sur le côté droit de l'écran. En outre, je définissais les insertions de section et le minimumInteritemSpacing dans les fonctions de délégué qui entraînaient des valeurs ne se reflétaient pas dans la classe personnalisée. Le correctif pour cela était de définir ces attributs sur une instance de la mise en page avant de l'appliquer à ma vue de collection.

Voici comment j'ai utilisé la mise en page pour ma vue de collection:

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)

Voici la classe de disposition de flux

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
la source
Vous avez sauvé ma journée
David 'mArm' Ansermot
0

Le problème avec UICollectionView est qu'il essaie d'ajuster automatiquement les cellules dans la zone disponible. J'ai fait cela en définissant d'abord le nombre de lignes et de colonnes, puis en définissant la taille de cellule pour cette ligne et cette colonne

1) Pour définir des sections (lignes) de mon UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Pour définir le nombre d'éléments dans une section. Vous pouvez définir un nombre différent d'articles pour chaque section. vous pouvez obtenir le numéro de section en utilisant le paramètre «section».

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

3) Pour définir la taille de cellule pour chaque section et ligne séparément. Vous pouvez obtenir le numéro de section et le numéro de ligne en utilisant le paramètre 'indexPath', c'est- [indexPath section]à- dire pour le numéro de section et [indexPath row]pour le numéro de ligne.

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

4) Ensuite, vous pouvez afficher vos cellules en lignes et en sections en utilisant:

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

REMARQUE: dans UICollectionView

Section == Row
IndexPath.Row == Column
Anas iqbal
la source
0

La réponse de Mike Sand est bonne mais j'avais rencontré des problèmes avec ce code (comme une longue cellule coupée). Et nouveau code:

#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
la source
0

Modification de la réponse d'Angel García Olloqui au respect de la part minimumInteritemSpacingdu délégué collectionView(_:layout:minimumInteritemSpacingForSectionAt:), si elle la met en œuvre.

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
la source
0

Le code ci-dessus fonctionne pour moi. Je souhaite partager le code Swift 3.0 respectif.

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
la source