horizontal ios uikit uicollectionview uicollectionviewlayout

ios - uicollectionview horizontal scroll programmatically swift



Paginación horizontal UICollectionView-¿Puedo usar diseño de flujo? (8)

¡Aquí comparto mi implementación simple!

El archivo .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

El archivo .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

Y finalmente, si quieres un comportamiento paginado, solo necesitas configurar tu UICollectionView:

_collectionView.pagingEnabled = YES;

Esperando ser lo suficientemente útil.

Esto está relacionado pero es distinto de ¿ Para usar Diseño de flujo o Personalizar? .

Aquí hay una ilustración de lo que estoy tratando de hacer:

Me pregunto si puedo hacer esto con UICollectionViewFlowLayout , una subclase del mismo, o si necesito crear un diseño completamente personalizado. Según los videos de la WWDC 2012 en UICollectionView, parece que si utiliza Flow Layout con desplazamiento vertical, sus líneas de diseño son horizontales, y si se desplaza horizontalmente, las líneas de diseño son verticales. Quiero líneas de diseño horizontal en una vista de colección de desplazamiento horizontal.

Tampoco tengo secciones inherentes en mi modelo; es solo un conjunto de elementos. Pude agruparlos en secciones, pero la vista de colección puede cambiarse de tamaño, por lo que el número de elementos que pueden caber en una página podría cambiar a veces, y parece que la opción de elegir qué página va cada elemento es mejor para el diseño que para el modelo si no tengo secciones significativas

Entonces, ¿puedo hacer esto con Flow Layout, o necesito crear un diseño personalizado?


La implementación anterior anterior no estaba completa, con fallas y con un tamaño de celda fijo. Aquí hay una traducción más literal para el código:

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


Se convirtió el código vilanovi a Swift en caso de que alguien lo necesite en el futuro.

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

}


Simplemente puede cambiar la dirección de desplazamiento en UICollectionView.xib a Horizontal. Y use con el orden correcto de elementos en la matriz.


Su último recurso, por supuesto, sería utilizar múltiples vistas de colección vertical dentro de cada sección en una vista de colección desplazable horizontalmente horizontal. Además de aumentar la complejidad del código y la dificultad para realizar animaciones de células entre secciones, no puedo pensar en grandes problemas con este enfoque de mi cabeza.


Tiene razón: así no es como una vista de colección de desplazamiento horizontal horizontal establece celdas. Me temo que tendrá que implementar su propia subclase UICollectionViewLayout personalizada. O eso, o separa tus modelos en secciones.


Swift 4

Código:

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

Esta es la versión de Swift 3 de la respuesta de @GuilhermeSprint

Código:

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

Uso:

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

Resultado

Extensión de mis delegados si quieres verificarlo también

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


@interface HorizontalCollectionViewLayout : UICollectionViewFlowLayout @end @implementation HorizontalCollectionViewLayout - (instancetype)init { self = [super init]; if (self) { self.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.minimumLineSpacing = 0; self.minimumInteritemSpacing = 0; } return self; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect]; NSInteger verticalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.height / self.itemSize.height); NSInteger horizontalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.width / self.itemSize.width); NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount; for (NSInteger i = 0; i < attributesArray.count; i++) { UICollectionViewLayoutAttributes *attributes = attributesArray[i]; NSInteger currentPage = (NSInteger)floor((double)i / (double)itemsPerPage); NSInteger currentRow = (NSInteger)floor((double)(i - currentPage * itemsPerPage) / (double)horizontalItemsCount); NSInteger currentColumn = i % horizontalItemsCount; CGRect frame = attributes.frame; frame.origin.x = self.itemSize.width * currentColumn + currentPage * self.collectionView.bounds.size.width; frame.origin.y = self.itemSize.height * currentRow; attributes.frame = frame; } return attributesArray; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return YES; } @end