web-dev-qa-db-fra.com

Pagination horizontale UICollectionView - puis-je utiliser Flow Layout?

Ceci est lié à/mais distinct de Pour utiliser la disposition du flux ou pour personnaliser? .

Voici une illustration de ce que j'essaie de faire: Illustration of what I’m trying to do

Je me demande si je peux le faire avec une UICollectionViewFlowLayout, une sous-classe de celle-ci, ou si je dois créer une mise en page entièrement personnalisée? D'après les vidéos WWDC 2012 sur UICollectionView, il semble que si vous utilisez la disposition de flux avec défilement vertical, vos lignes de présentation sont horizontales et si vous faites défiler horizontalement, vos lignes de présentation sont verticales. Je veux des lignes de disposition horizontales dans une vue de collection à défilement horizontal.

Je n’ai pas non plus de sections inhérentes à mon modèle - c’est un ensemble unique d’éléments. Je pourrais les regrouper en sections, mais la vue de la collection est redimensionnable. Le nombre d'éléments pouvant tenir sur une page change parfois, et il semble que le choix de la page à laquelle chaque élément est attribué est mieux laissé à la mise en page qu'à le modèle si je n'ai pas de sections significatives.

Donc, puis-je le faire avec Flow Layout ou dois-je créer une présentation personnalisée?

42
Zev Eisenberg

Vous avez raison - ce n'est pas ainsi qu'une vue de collection stock-scroll horizontal stocke des cellules. Je crains que vous n'ayez à implémenter votre propre sous-classe UICollectionViewLayout personnalisée. Soit ça, ou séparez vos modèles en sections. 

12
Ash Furrow

Ici, je partage ma mise en œuvre simple! 

Le fichier .h:

/** 
 * CollectionViewLayout for an horizontal flow type:
 *
 *  |   0   1   |   6   7   |
 *  |   2   3   |   8   9   |   ----> etc...
 *  |   4   5   |   10  11  |
 *
 * Only supports 1 section and no headers, footers or decorator views.
 */
@interface HorizontalCollectionViewLayout : UICollectionViewLayout

@property (nonatomic, assign) CGSize itemSize;

@end

Le fichier .m:

@implementation HorizontalCollectionViewLayout
{
    NSInteger _cellCount;
    CGSize _boundsSize;
}

- (void)prepareLayout
{
    // Get the number of cells and the bounds size
    _cellCount = [self.collectionView numberOfItemsInSection:0];
    _boundsSize = self.collectionView.bounds.size;
}

- (CGSize)collectionViewContentSize
{
    // We should return the content size. Lets do some math:

    NSInteger verticalItemsCount = (NSInteger)floorf(_boundsSize.height / _itemSize.height);
    NSInteger horizontalItemsCount = (NSInteger)floorf(_boundsSize.width / _itemSize.width);

    NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
    NSInteger numberOfItems = _cellCount;
    NSInteger numberOfPages = (NSInteger)ceilf((CGFloat)numberOfItems / (CGFloat)itemsPerPage);

    CGSize size = _boundsSize;
    size.width = numberOfPages * _boundsSize.width;
    return size;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    // This method requires to return the attributes of those cells that intsersect with the given rect.
    // In this implementation we just return all the attributes.
    // In a better implementation we could compute only those attributes that intersect with the given rect.

    NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:_cellCount];

    for (NSUInteger i=0; i<_cellCount; ++i)
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        UICollectionViewLayoutAttributes *attr = [self _layoutForAttributesForCellAtIndexPath:indexPath];

        [allAttributes addObject:attr];
    }

    return allAttributes;
}

- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [self _layoutForAttributesForCellAtIndexPath:indexPath];
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    // We should do some math here, but we are lazy.
    return YES;
}

- (UICollectionViewLayoutAttributes*)_layoutForAttributesForCellAtIndexPath:(NSIndexPath*)indexPath
{
    // Here we have the magic of the layout.

    NSInteger row = indexPath.row;

    CGRect bounds = self.collectionView.bounds;
    CGSize itemSize = self.itemSize;

    // Get some info:
    NSInteger verticalItemsCount = (NSInteger)floorf(bounds.size.height / itemSize.height);
    NSInteger horizontalItemsCount = (NSInteger)floorf(bounds.size.width / itemSize.width);
    NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;

    // Compute the column & row position, as well as the page of the cell.
    NSInteger columnPosition = row%horizontalItemsCount;
    NSInteger rowPosition = (row/horizontalItemsCount)%verticalItemsCount;
    NSInteger itemPage = floorf(row/itemsPerPage);

    // Creating an empty attribute
    UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGRect frame = CGRectZero;

    // And finally, we assign the positions of the cells
    frame.Origin.x = itemPage * bounds.size.width + columnPosition * itemSize.width;
    frame.Origin.y = rowPosition * itemSize.height;
    frame.size = _itemSize;

    attr.frame = frame;

    return attr;
}

#pragma mark Properties

- (void)setItemSize:(CGSize)itemSize
{
    _itemSize = itemSize;
    [self invalidateLayout];
}

@end

Et enfin, si vous voulez un comportement paginé, il vous suffit de configurer votre UICollectionView:

_collectionView.pagingEnabled = YES;

En espérant être assez utile.

34
vilanovi

Converti le code vilanovi en Swift au cas où quelqu'un en aurait besoin à l'avenir.

public class HorizontalCollectionViewLayout : UICollectionViewLayout {
private var cellWidth = 90 // Don't kow how to get cell size dynamically
private var cellHeight = 90

public override func prepareLayout() {
}

public override func collectionViewContentSize() -> CGSize {
    let numberOfPages = Int(ceilf(Float(cellCount) / Float(cellsPerPage)))
    let width = numberOfPages * Int(boundsWidth)
    return CGSize(width: CGFloat(width), height: boundsHeight)
}

public override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
    var allAttributes = [UICollectionViewLayoutAttributes]()

    for (var i = 0; i < cellCount; ++i) {
        let indexPath = NSIndexPath(forRow: i, inSection: 0)
        let attr = createLayoutAttributesForCellAtIndexPath(indexPath)
        allAttributes.append(attr)
    }

    return allAttributes
}

public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
    return createLayoutAttributesForCellAtIndexPath(indexPath)
}

public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
}

private func createLayoutAttributesForCellAtIndexPath(indexPath:NSIndexPath)
    -> UICollectionViewLayoutAttributes {
        let layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
        layoutAttributes.frame = createCellAttributeFrame(indexPath.row)
        return layoutAttributes
}

private var boundsWidth:CGFloat {
    return self.collectionView!.bounds.size.width
}

private var boundsHeight:CGFloat {
    return self.collectionView!.bounds.size.height
}

private var cellCount:Int {
    return self.collectionView!.numberOfItemsInSection(0)
}

private var verticalCellCount:Int {
    return Int(floorf(Float(boundsHeight) / Float(cellHeight)))
}

private var horizontalCellCount:Int {
    return Int(floorf(Float(boundsWidth) / Float(cellWidth)))
}

private var cellsPerPage:Int {
    return verticalCellCount * horizontalCellCount
}

private func createCellAttributeFrame(row:Int) -> CGRect {
    let frameSize = CGSize(width:cellWidth, height: cellHeight )
    let frameX = calculateCellFrameHorizontalPosition(row)
    let frameY = calculateCellFrameVerticalPosition(row)
    return CGRectMake(frameX, frameY, frameSize.width, frameSize.height)
}

private func calculateCellFrameHorizontalPosition(row:Int) -> CGFloat {
    let columnPosition = row % horizontalCellCount
    let cellPage = Int(floorf(Float(row) / Float(cellsPerPage)))
    return CGFloat(cellPage * Int(boundsWidth) + columnPosition * Int(cellWidth))
}

private func calculateCellFrameVerticalPosition(row:Int) -> CGFloat {
    let rowPosition = (row / horizontalCellCount) % verticalCellCount
    return CGFloat(rowPosition * Int(cellHeight))
}

}

14
Pedro Santos

La précédente implémentation ci-dessus n’était pas complète, complexe et avec une taille de cellule fixe. Voici une traduction plus littérale du code:

import UIKit

class HorizontalFlowLayout: UICollectionViewLayout {
    var itemSize = CGSizeZero {
        didSet {
            invalidateLayout()
        }
    }
    private var cellCount = 0
    private var boundsSize = CGSizeZero

    override func prepareLayout() {
        cellCount = self.collectionView!.numberOfItemsInSection(0)
        boundsSize = self.collectionView!.bounds.size
    }

    override func collectionViewContentSize() -> CGSize {
        let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
        let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))

        let itemsPerPage = verticalItemsCount * horizontalItemsCount
        let numberOfItems = cellCount
        let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))

        var size = boundsSize
        size.width = CGFloat(numberOfPages) * boundsSize.width
        return size
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var allAttributes = [UICollectionViewLayoutAttributes]()
        for var i = 0; i < cellCount; i++ {
            let indexPath = NSIndexPath(forRow: i, inSection: 0)
            let attr = self.computeLayoutAttributesForCellAtIndexPath(indexPath)
            allAttributes.append(attr)
        }
        return allAttributes
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        return self.computeLayoutAttributesForCellAtIndexPath(indexPath)
    }

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

    func computeLayoutAttributesForCellAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes {
        let row = indexPath.row
        let bounds = self.collectionView!.bounds

        let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
        let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
        let itemsPerPage = verticalItemsCount * horizontalItemsCount

        let columnPosition = row % horizontalItemsCount
        let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
        let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))

        let attr = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)

        var frame = CGRectZero
        frame.Origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
        frame.Origin.y = CGFloat(rowPosition) * itemSize.height
        frame.size = itemSize
        attr.frame = frame

        return attr
    }
}
3
Guilherme Sprint

Swift 4

Code:

public class HorizontalFlowLayout: UICollectionViewLayout {
    var itemSize = CGSize(width: 0, height: 0) {
        didSet {
            invalidateLayout()
        }
    }
    private var cellCount = 0
    private var boundsSize = CGSize(width: 0, height: 0)

    public override func prepare() {
        cellCount = self.collectionView!.numberOfItems(inSection: 0)
        boundsSize = self.collectionView!.bounds.size
    }
    public override var collectionViewContentSize: CGSize {
        let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
        let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))

        let itemsPerPage = verticalItemsCount * horizontalItemsCount
    let numberOfItems = cellCount
    let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))

        var size = boundsSize
        size.width = CGFloat(numberOfPages) * boundsSize.width
        return size
    }

    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var allAttributes = [UICollectionViewLayoutAttributes]()
        for i in 0...(cellCount-1) {
            let indexPath = IndexPath(row: i, section: 0)
            let attr = self.computeLayoutAttributesForCellAt(indexPath: indexPath)
            allAttributes.append(attr)
        }
        return allAttributes
    }

    public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return computeLayoutAttributesForCellAt(indexPath: indexPath)
    }

    public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

    private func computeLayoutAttributesForCellAt(indexPath:IndexPath)
        -> UICollectionViewLayoutAttributes {
            let row = indexPath.row
            let bounds = self.collectionView!.bounds

            let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
            let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
            let itemsPerPage = verticalItemsCount * horizontalItemsCount

            let columnPosition = row % horizontalItemsCount
            let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
            let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))

            let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)

            var frame = CGRect(x: 0, y: 0, width: 0, height: 0)
            frame.Origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
            frame.Origin.y = CGFloat(rowPosition) * itemSize.height
            frame.size = itemSize
            attr.frame = frame

            return attr
    }
}

Ceci est la version Swift 3 de la réponse @GuilhermeSprint 

Code:

public class HorizontalCollectionViewLayout : UICollectionViewLayout {
    var itemSize = CGSize(width: 0, height: 0) {
        didSet {
            invalidateLayout()
        }
    }
    private var cellCount = 0
    private var boundsSize = CGSize(width: 0, height: 0)

    public override func prepare() {
        cellCount = self.collectionView!.numberOfItems(inSection: 0)
        boundsSize = self.collectionView!.bounds.size
    }
    public override var collectionViewContentSize: CGSize {
        let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
        let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))

        let itemsPerPage = verticalItemsCount * horizontalItemsCount
        let numberOfItems = cellCount
        let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))

        var size = boundsSize
        size.width = CGFloat(numberOfPages) * boundsSize.width
        return size
    }

    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var allAttributes = [UICollectionViewLayoutAttributes]()
        for i in 0...(cellCount-1) {
            let indexPath = IndexPath(row: i, section: 0)
            let attr = self.computeLayoutAttributesForCellAt(indexPath: indexPath)
            allAttributes.append(attr)
        }
        return allAttributes
    }

    public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return computeLayoutAttributesForCellAt(indexPath: indexPath)
    }

    public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

    private func computeLayoutAttributesForCellAt(indexPath:IndexPath)
        -> UICollectionViewLayoutAttributes {
            let row = indexPath.row
            let bounds = self.collectionView!.bounds

            let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
            let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
            let itemsPerPage = verticalItemsCount * horizontalItemsCount

            let columnPosition = row % horizontalItemsCount
            let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
            let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))

            let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)

            var frame = CGRectMake(0, 0, 0, 0)
            frame.Origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
            frame.Origin.y = CGFloat(rowPosition) * itemSize.height
            frame.size = itemSize
            attr.frame = frame

            return attr
    }
}

Usage:

    // I want to have 4 items in the page / see screenshot below
    let itemWidth = collectionView.frame.width / 2.0
    let itemHeight = collectionView.frame.height / 2.0
    let horizontalCV = HorizontalCollectionViewLayout();
    horizontalCV.itemSize = CGSize(width: itemWidth, height: itemHeight)
    collectionView.collectionViewLayout = horizontalCV

Résultat

 screenshot

L'extension Mes délégués si vous voulez vérifier aussi

extension MyViewController : UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
    // Delegate
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Clicked")
    }

    // DataSource
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BottomMenuCCell.xib, for: indexPath) as? BottomMenuCCell {
            cell.ibi = bottomMenuButtons[indexPath.row]
            cell.layer.borderWidth = 0
            return cell
        }
        return BaseCollectionCell()
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize.init(width: (collectionView.width / 2.0), height: collectionView.height / 2.0)
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return bottomMenuButtons.count
    }

    // removing spacing between items
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0.0
    }
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0.0
    }
}
2
MBH

Peut simplement changer le sens de défilement dans UICollectionView.xib en Horizontal. Et utilisez avec le bon ordre d'éléments dans le tableau.

0
Alexander