UICollectionView Definir número de colunas

100

Acabei de começar a aprender sobre UICollectionViews. Estou me perguntando se alguém sabe como especificar o número de colunas em um collectionview. O padrão é 3 (iPhone / retrato). Eu olhei a documentação e não consigo encontrar uma resposta concisa.

SNV7
fonte
1
Tenho um aplicativo que uso UICollectionViewbasicamente como grade porque sei que só quero 4 colunas. Eu faço isso tornando minhas células de um certo tamanho e o espaçamento entre elas de um certo tamanho para que a visualização da coleção mostre apenas 4 colunas, não importa a orientação. É semelhante ao que você precisa?
jhilgert00
Ou é mais uma questão geral, como posso definir dinamicamente o número de colunas ou não?
jhilgert00
Basicamente, procuro ter 7 colunas no modo retrato. Portanto, fiz a largura das colunas de 46 px (320 px / 46px = 7). Eu não sabia que era uma propriedade de espaçamento.
SNV7

Respostas:

81

CollectionViews são muito poderosos e têm um preço. Muitas e muitas opções. Como omz disse:

Existem várias maneiras de alterar o número de colunas

Eu sugiro implementar o <UICollectionViewDelegateFlowLayout>Protocolo, dando a você acesso aos seguintes métodos nos quais você pode ter maior controle sobre o layout do seu UICollectionView, sem a necessidade de subclassificá-lo:

  • collectionView:layout:insetForSectionAtIndex:
  • collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
  • collectionView:layout:minimumLineSpacingForSectionAtIndex:
  • collectionView:layout:referenceSizeForFooterInSection:
  • collectionView:layout:referenceSizeForHeaderInSection:
  • collectionView:layout:sizeForItemAtIndexPath:

Além disso, implementar o método a seguir forçará seu UICollectionView a atualizar seu layout em uma mudança de orientação: (digamos que você queira redimensionar as células para paisagem e aumentá-las)

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                               duration:(NSTimeInterval)duration{

    [self.myCollectionView.collectionViewLayout invalidateLayout];
}

Além disso, aqui estão 2 tutoriais realmente bons sobre UICollectionViews:

http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12

http://skeuo.com/uicollectionview-custom-layout-tutorial

jhilgert00
fonte
1
Isso funcionará para todos os dispositivos iOS? Deve collectionView:layout:sizeForItemAtIndexPath:ser manuseado para todos os dispositivos separadamente?
Sharath,
1
@Sharath, é claro que funciona para tudo. Você só precisa fazer suas contas de forma que seja adaptável a todos os dispositivos;). Design adaptativo.
TheCodingArt
uau tão bom, mas exemplo real? no Android podemos fazer new GridLayoutManager(mContext, 4))(4 colunas), muito fácil, ou apenas usar LinearLayoutManager(é claro que tem apenas 1 coluna)
usuário 25 de
145

Com o Swift 5 e iOS 12.3, você pode usar uma das 4 implementações a seguir para definir o número de itens por linha em seu, UICollectionViewenquanto gerencia inserções e alterações de tamanho (incluindo rotação).


# 1. Subclassificando UICollectionViewFlowLayoute usando UICollectionViewFlowLayouta itemSizepropriedade de

ColumnFlowLayout.swift:

import UIKit

class ColumnFlowLayout: UICollectionViewFlowLayout {

    let cellsPerRow: Int

    init(cellsPerRow: Int, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        self.cellsPerRow = cellsPerRow
        super.init()

        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }
        let marginsAndInsets = sectionInset.left + sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
        itemSize = CGSize(width: itemWidth, height: itemWidth)
    }

    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
        let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
        context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
        return context
    }

}

CollectionViewController.swift:

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = ColumnFlowLayout(
        cellsPerRow: 5,
        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(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 59
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.backgroundColor = UIColor.orange
        return cell
    }

}

insira a descrição da imagem aqui


# 2. Usando UICollectionViewFlowLayouto itemSizemétodo de

import UIKit

class CollectionViewController: UICollectionViewController {

    let margin: CGFloat = 10
    let cellsPerRow = 5

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let collectionView = collectionView, let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { return }

        flowLayout.minimumInteritemSpacing = margin
        flowLayout.minimumLineSpacing = margin
        flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)

        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func viewWillLayoutSubviews() {
        guard let collectionView = collectionView, let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + flowLayout.minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
        flowLayout.itemSize =  CGSize(width: itemWidth, height: itemWidth)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 59
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.backgroundColor = UIColor.orange
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

# 3. Usando UICollectionViewDelegateFlowLayouto collectionView(_:layout:sizeForItemAt:)método de

import UIKit

class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    let inset: CGFloat = 10
    let minimumLineSpacing: CGFloat = 10
    let minimumInteritemSpacing: CGFloat = 10
    let cellsPerRow = 5

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return minimumLineSpacing
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return minimumInteritemSpacing
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let marginsAndInsets = inset * 2 + collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
        let itemWidth = ((collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
        return CGSize(width: itemWidth, height: itemWidth)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 59
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.backgroundColor = UIColor.orange
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

# 4. Subclassificando UICollectionViewFlowLayoute usando UICollectionViewFlowLayouta estimatedItemSizepropriedade de

CollectionViewController.swift:

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
        "Lorem ipsum dolor sit amet, consectetur.",
        "Lorem ipsum dolor sit amet.",
        "Lorem ipsum dolor sit amet, consectetur.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing.",
        "Lorem ipsum.",
        "Lorem ipsum dolor sit amet.",
        "Lorem ipsum dolor sit.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.",
        "Lorem ipsum dolor sit amet, consectetur."
    ]

    let columnLayout = FlowLayout(
        cellsPerRow: 3,
        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(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.label.text = items[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 {

    let cellsPerRow: Int

    required init(cellsPerRow: Int = 1, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        self.cellsPerRow = cellsPerRow

        super.init()

        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = collectionView else { return layoutAttributes }

        let marginsAndInsets = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + sectionInset.left + sectionInset.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
        layoutAttributes.bounds.size.width = ((collectionView.bounds.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)

        return layoutAttributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let superLayoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return superLayoutAttributes }

        let layoutAttributes = superLayoutAttributes.compactMap { layoutAttribute in
            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
        }

        // (optional) Uncomment to top align cells that are on the same line
        /*
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            guard let max = attributes.max(by: { $0.size.height < $1.size.height }) else { continue }
            for attribute in attributes where attribute.size.height != max.size.height {
                attribute.frame.origin.y = max.frame.origin.y
            }
        }
         */

        // (optional) Uncomment to bottom align cells that are on the same line
        /*
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            guard let max = attributes.max(by: { $0.size.height < $1.size.height }) else { continue }
            for attribute in attributes where attribute.size.height != max.size.height {
                attribute.frame.origin.y += max.frame.maxY - attribute.frame.maxY
            }
        }
         */

        return layoutAttributes
    }

}

Cell.swift:

import UIKit

class Cell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.numberOfLines = 0
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        layoutIfNeeded()
        label.preferredMaxLayoutWidth = label.bounds.size.width
        layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        return layoutAttributes
    }

    // Alternative implementation
    /*
    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.right
        layoutAttributes.bounds.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        return layoutAttributes
    }
    */

}

insira a descrição da imagem aqui

Imanou Petit
fonte
3
As soluções que usam "viewWillTransition (to size ..." não manipulam a rotação do dispositivo corretamente. Como é chamado antes de o quadro ser definido, a dimensão antiga de collectionView é usada para calcular o tamanho da célula. (Não tentei o outras soluções ainda)
vale
2
Usei a solução nº 1, você é meu novo herói.
Tom Wolters
1
Usei sua solução # 1. É excelente.
Shahil 02 de
1
resposta ótima e completa, implementada # 2
conduzida em
1
A solução # 1 é perfeita! Obrigado pela contribuição!
Brian,
61

Implementei UICollectionViewDelegateFlowLayoutno meu UICollectionViewControllere substituí o método responsável por determinar o tamanho da célula. Em seguida, peguei a largura da tela e dividi-a com o requisito da coluna. Por exemplo, eu queria ter 3 colunas em cada tamanho de tela. Então, aqui está a aparência do meu código -

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CGRect screenRect = [[UIScreen mainScreen] bounds];
    CGFloat screenWidth = screenRect.size.width;
    float cellWidth = screenWidth / 3.0; //Replace the divisor with the column count requirement. Make sure to have it in float.
    CGSize size = CGSizeMake(cellWidth, cellWidth);

    return size;
}
novato
fonte
2
Muito mais simples, obrigado pela sanidade e menos código
jjxtra
2
Você deve certificar-se de que o espaçamento do item está definido como 0 para evitar ter n-1 itens altamente espaçados por linha.
Joe de
41

Expandindo a resposta de Noob:

func collectionView(collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {

        let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
        let totalSpace = flowLayout.sectionInset.left
            + flowLayout.sectionInset.right
            + (flowLayout.minimumInteritemSpacing * CGFloat(numberOfItemsPerRow - 1))
        let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(numberOfItemsPerRow))
        return CGSize(width: size, height: size)
}

Isso permite qualquer espaçamento entre as células. Ele assume uma Intvariável de membro chamada numberOfItemsPerRowe também que todas as células são quadradas e têm o mesmo tamanho. Conforme observado na resposta de jhilgert00, também devemos reagir às mudanças de orientação, mas agora usando viewWillTransitionToSizecomo willRotateToInterfaceOrientationé depreciado.

sgib
fonte
let size = Int ((collectionView.bounds.width - totalSpace) / CGFloat (numberOfItemsPerRow)) - você deve remover a conversão para Int, porque em alguns casos ele
cria
14

Aqui está o código de trabalho para Swift 3, para ter um layout de duas colunas:

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {

    let nbCol = 2

    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    let totalSpace = flowLayout.sectionInset.left
        + flowLayout.sectionInset.right
        + (flowLayout.minimumInteritemSpacing * CGFloat(nbCol - 1))
    let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(nbCol))
    return CGSize(width: size, height: size)
}

Sinta-se à vontade para alterar "nbCol" para o número de colunas desejado.

fraxool
fonte
o que há de errado com todas as soluções quando as inserções são (1.0,1.0,1.0,1.0), o layout torna-se 1 coluna neste caso!
rd_
@fraxool Ótima resposta, mas se eu virar o dispositivo o layout é redefinido ... Alguma correção para isso?
Maksim Kniazev
8

Se você for preguiçoso, use delegado.

extension UICollectionView {
    func setItemsInRow(items: Int) {
        if let layout = self.collectionViewLayout as? UICollectionViewFlowLayout {
            let contentInset = self.contentInset
            let itemsInRow: CGFloat = CGFloat(items);
            let innerSpace = layout.minimumInteritemSpacing * (itemsInRow - 1.0)
            let insetSpace = contentInset.left + contentInset.right + layout.sectionInset.left + layout.sectionInset.right
            let width = floor((CGRectGetWidth(frame) - insetSpace - innerSpace) / itemsInRow);
            layout.itemSize = CGSizeMake(width, width)
        }
    }
}

PS: Deve ser chamado após a rotação também

HotJard
fonte
2
Funciona em Swift 4.0 também ... após as alterações das duas últimas linhas: let width = floor ((frame.width - insetSpace - innerSpace) / itemsInRow) layout.itemSize = CGSize (largura: largura, altura: largura)
g212gs
8

Atualizado para Swift 3:

Em vez do layout de fluxo, prefiro usar o layout personalizado para o número da coluna e o número da linha específicos. Porque:

  1. Pode ser arrastado horizontalmente se o número da coluna for muito grande.
  2. É mais aceitável logicamente por causa do uso de coluna e linha.

Célula normal e célula de cabeçalho: (Adicione UILabel como um IBOutlet ao seu xib):

class CollectionViewCell: UICollectionViewCell {

@IBOutlet weak var label: UILabel!

override func awakeFromNib() {
    super.awakeFromNib()
    // Initialization code
    self.backgroundColor = UIColor.black
    label.textColor = UIColor.white
}
}

class CollectionViewHeadCell: UICollectionViewCell {

@IBOutlet weak var label: UILabel!

override func awakeFromNib() {
    super.awakeFromNib()
    // Initialization code
    self.backgroundColor = UIColor.darkGray
    label.textColor = UIColor.white
}
}

Layout personalizado:

let cellHeight: CGFloat = 100
let cellWidth: CGFloat = 100

class CustomCollectionViewLayout: UICollectionViewLayout {
    private var numberOfColumns: Int!
    private var numberOfRows: Int!

    // It is two dimension array of itemAttributes
    private var itemAttributes = [[UICollectionViewLayoutAttributes]]()
    // It is one dimension of itemAttributes
    private var cache = [UICollectionViewLayoutAttributes]()

override func prepare() {
    if self.cache.isEmpty {

        self.numberOfColumns = self.collectionView?.numberOfItems(inSection: 0)
        self.numberOfRows = self.collectionView?.numberOfSections

        // Dynamically change cellWidth if total cell width is smaller than whole bounds
        /* if (self.collectionView?.bounds.size.width)!/CGFloat(self.numberOfColumns) > cellWidth {
         self.cellWidth = (self.collectionView?.bounds.size.width)!/CGFloat(self.numberOfColumns)
         }
         */
        for row in 0..<self.numberOfRows {
            var row_temp = [UICollectionViewLayoutAttributes]()
            for column in 0..<self.numberOfColumns {

                let indexPath = NSIndexPath(item: column, section: row)

                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
                attributes.frame = CGRect(x: cellWidth*CGFloat(column), y: cellHeight*CGFloat(row), width: cellWidth, height: cellHeight)

                row_temp.append(attributes)

                self.cache.append(attributes)
            }
            self.itemAttributes.append(row_temp)
        }
    }
}
override var collectionViewContentSize: CGSize {
    return CGSize(width: CGFloat(self.numberOfColumns)*cellWidth, height: CGFloat(self.numberOfRows)*cellHeight)
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    var layoutAttributes = [UICollectionViewLayoutAttributes]()

    for attributes in cache {
        if attributes.frame.intersects(rect) {
            layoutAttributes.append(attributes)
        }
    }
    return layoutAttributes
}
}

CollectionView:

let CellIdentifier = "CellIdentifier"
let HeadCellIdentifier = "HeadCellIdentifier"

class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {

init() {
    let layout = CustomCollectionViewLayout()

    super.init(frame: CGRect.zero, collectionViewLayout: layout)

    self.register(UINib(nibName: "CollectionViewCell", bundle: nil), forCellWithReuseIdentifier: CellIdentifier)
    self.register(UINib(nibName: "CollectionViewHeadCell", bundle: nil), forCellWithReuseIdentifier: HeadCellIdentifier)

    self.isDirectionalLockEnabled = true
    self.dataSource = self
    self.delegate = self
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func updateCollectionView() {
    DispatchQueue.main.async {
        self.reloadData()
    }
}

// MARK: CollectionView datasource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 20
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 20
}
override func numberOfItems(inSection section: Int) -> Int {
    return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let column = (indexPath as NSIndexPath).row
    let row = (indexPath as NSIndexPath).section

    if column == 0 {
        let cell : CollectionViewHeadCell = collectionView.dequeueReusableCell(withReuseIdentifier: HeadCellIdentifier, for: indexPath) as! CollectionViewHeadCell

        cell.label.text = "\(row)"

        return cell
    }
    else if row == 0 {
        let cell : CollectionViewHeadCell = collectionView.dequeueReusableCell(withReuseIdentifier: HeadCellIdentifier, for: indexPath) as! CollectionViewHeadCell

        cell.label.text = "\(column)"

        return cell
    }
    else {
        let cell : CollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier, for: indexPath) as! CollectionViewCell

        cell.label.text = String(format: "%d", arguments: [indexPath.section*indexPath.row])

        return cell
    }
}

// MARK: CollectionView delegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    let column = (indexPath as NSIndexPath).row
    let row = (indexPath as NSIndexPath).section

    print("\(column)  \(row)")
}
}

Use CollectionView do ViewController:

class ViewController: UIViewController {
let collectionView = CollectionView()

override func viewDidLoad() {
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(collectionView)
    self.view.backgroundColor = UIColor.red

    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[collectionView]|", options: [], metrics: nil, views: ["collectionView": collectionView]))
    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[collectionView]|", options: [], metrics: nil, views: ["collectionView": collectionView]))
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    collectionView.updateCollectionView()
}
}

Finalmente, você pode ter um CollectionView sofisticado!

insira a descrição da imagem aqui

brianLikeApple
fonte
o problema em tomar esta solução é sua altura e largura estáticas
rd_
2
O layout @rd_Custom significa que você pode fazer o que quiser! Meu código está mostrando o caso mais simples ......
brianLikeApple
7

Por UICollectionViewser tão flexível, existem várias maneiras de alterar o número de colunas, dependendo do tipo de layout usado.

O UICollectionViewFlowLayout(que é provavelmente com o que você está trabalhando) não especifica um número de colunas diretamente (porque depende do tamanho / orientação da visualização). A maneira mais fácil de alterar seria definir a itemSizepropriedade e / ou minimumInteritemSpacing/ minimumLineSpacing.

omz
fonte
4

Swift 3.0. Funciona para direções de rolagem horizontal e vertical e espaçamento variável

Especifique o número de colunas

let numberOfColumns: CGFloat = 3

Configure flowLayoutpara renderizar especificadonumberOfColumns

if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
    let horizontalSpacing = flowLayout.scrollDirection == .vertical ? flowLayout.minimumInteritemSpacing : flowLayout.minimumLineSpacing
    let cellWidth = (collectionView.frame.width - max(0, numberOfColumns - 1)*horizontalSpacing)/numberOfColumns
    flowLayout.itemSize = CGSize(width: cellWidth, height: cellWidth)
}
Jože Ws
fonte
Todas aquelas longas respostas em todos os lugares e isso funciona com 5 linhas de código :)
J. Doe
1
Boa tentativa. Mas isso não funcionará se você usar um sectionInset no collectionViewLayout. Para corrigir isso, (1) calcule a largura disponível tomando a largura da célula e subtraindo flowLayout.sectionInset.left e flowLayout.sectionInset.right dela. (2) Onde você calcula cellWidth, substitua 'view.frame.width' pelo valor calculado em (1). Além disso, lembre-se de que o quadro da visualização só será finalizado após o carregamento e pode mudar, portanto, você terá que reconstruir o layout conforme necessário.
Womble
4

Atualizado para Swift 5+ iOS 13

O tamanho estimado da visualização da coleção deve ser nenhum

insira a descrição da imagem aqui

Declarar margem para célula

let margin: CGFloat = 10

Na viewDidLoad configure minimumInteritemSpacing, minimumLineSpacing,sectionInset

 guard let collectionView = docsColl, let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }

    flowLayout.minimumInteritemSpacing = margin
    flowLayout.minimumLineSpacing = margin
    flowLayout.sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)

Método UICollectionViewDataSourcesizeForItemAt

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    let noOfCellsInRow = 2   //number of column you want
    let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
    let totalSpace = flowLayout.sectionInset.left
        + flowLayout.sectionInset.right
        + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1))

    let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(noOfCellsInRow))
    return CGSize(width: size, height: size)
}
Sandeep Maurya
fonte
2

É tudo sobre o layout que você deseja desenhar. Você pode criar uma classe personalizada herdada de UICollectionViewFlowLayout. Atualmente não existe nenhum método direto para definir colunas. Se você deseja obter este tipo de funcionalidade, você precisa fazê-lo manualmente. Você precisa lidar com isso em sua classe de layout de fluxo customizado.

Agora surgirá a questão de como você fará isso? Se você não quiser perturbar o quadro da célula, você pode ajustar

 collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
 collectionView:layout:minimumLineSpacingForSectionAtIndex:

Outra maneira é fornecer suas próprias posições de células. Substituindo os dois métodos abaixo, que serão chamados durante a formação do layout.

  - (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
  - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path

UICollectionViewLayoutAttributes é uma classe que lidará com a posição da célula, quadro, Zindex etc.

Swapnil
fonte
2

Esta resposta é para swift 3.0 ->

primeiro conformar-se com o protocolo:

 UICollectionViewDelegateFlowLayout

em seguida, adicione estes métodos:

    //this method is for the size of items      
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.frame.width/3
        let height : CGFloat = 160.0
        return CGSize(width: width, height: height)
    }
    //these methods are to configure the spacing between items

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsetsMake(0,0,0,0)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }
MhmdRizk
fonte
1

Eu só queria acrescentar a resposta nº 2 de Imanou Petit. Para garantir que as margens sejam exatas, independentemente da largura da tela, uso um solucionador iterativo com uma margem desejada e o número de colunas como entradas. Eu também adicionei um sinalizador direcional em que suas margens finais serão comparadas ao seu destino.

O solucionador iterativo é mostrado abaixo e retorna cellWidth e margin.

private func iterativeCellSpacing(targetMargins : CGFloat,
                                  cellsPerRow : Int,
                                  isMinTarget : Bool) -> (CGFloat, CGFloat)
{
    var w : CGFloat = 0
    var m : CGFloat = targetMargins
    let cols : CGFloat = CGFloat(cellsPerRow)

    let numMargins : CGFloat = cols + 1.0
    let screenWidth : CGFloat = collectionView!.bounds.size.width


    var delta = CGFloat.greatestFiniteMagnitude
    while abs(delta) > 0.001
    {
        let totalMarginSpacing = numMargins * m
        let totalCellSpacing = screenWidth - totalMarginSpacing

        if (isMinTarget)
        {
            w = floor(totalCellSpacing / cols)
            m = ceil((screenWidth - cols * w) / numMargins)
        }
        else
        {
            w = ceil(totalCellSpacing / cols)
            m = floor((screenWidth - cols * w) / numMargins)
        }

        delta = screenWidth - w * CGFloat(cellsPerRow) - m * numMargins
    }

    return (w, m)
}

Eu chamo assim:

fileprivate var margin: CGFloat = 20
fileprivate var cellWidth : CGFloat = 80
fileprivate let cellsPerRow = 4

override func viewDidLoad()
{
    super.viewDidLoad()

    (cellWidth, margin) = iterativeCellSpacing(targetMargins: margin, cellsPerRow: 4, isMinTarget: true)
    ...
}

Em seguida, aplico os valores de cellWidth e margin ao layout de fluxo como:

extension MyCollectionController : UICollectionViewDelegateFlowLayout

{func collectionView (_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {return CGSize (width: cellWidth, height: cellWidth)}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    collectionView?.collectionViewLayout.invalidateLayout()
    super.viewWillTransition(to: size, with: coordinator)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
    return UIEdgeInsetsMake(margin, margin, margin, margin)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
    return margin
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
{
    return margin
}

}

Espero que isto ajude. Provavelmente existe uma maneira mais fácil de garantir que as margens sejam exatas, mas esse é um método. Além disso, este código não foi testado para dispositivos que permitem visualizações de coleção rotativas.

Obrigado,

Jan-Michael Tressler
fonte
0

a solução perfeita é usar UICollectionViewDelegateFlowLayout, mas você pode facilmente calcular a largura da célula e dividir no número desejado de colunas que você deseja

o complicado é fazer a largura sem fração

(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

{
 CGFloat screenWidth = self.view.frame.size.width;
   CGFloat marginWidth = (screenWidth - collectionView.frame.size.width);


   CGFloat cellWith = (collectionView.frame.size.width - marginWidth )/3;
   cellWith= floorf(cellWith);




  CGSize retval = CGSizeMake(cellWith,cellWith);


  return retval;}
Amr irritado
fonte
0

Fiz um layout de coleção.

Para tornar o separador visível, defina a cor de fundo da visualização da coleção como cinza. Uma linha por seção.

Uso:

let layout = GridCollectionViewLayout()
layout.cellHeight = 50 // if not set, cellHeight = Collection.height/numberOfSections
layout.cellWidth = 50  // if not set, cellWidth = Collection.width/numberOfItems(inSection)
collectionView.collectionViewLayout = layout

Layout:

import UIKit

class GridCollectionViewLayout: UICollectionViewLayout {


var cellWidth : CGFloat = 0
var cellHeight : CGFloat = 0
var seperator: CGFloat = 1

private var cache = [UICollectionViewLayoutAttributes]()



override func prepare() {

    guard let collectionView = self.collectionView else {
        return
    }

    self.cache.removeAll()



        let numberOfSections = collectionView.numberOfSections

        if cellHeight <= 0
        {
            cellHeight = (collectionView.bounds.height - seperator*CGFloat(numberOfSections-1))/CGFloat(numberOfSections)
        }

        for section in 0..<collectionView.numberOfSections {

            let numberOfItems = collectionView.numberOfItems(inSection: section)

            let cellWidth2 : CGFloat
            if cellWidth <= 0
            {
                cellWidth2 = (collectionView.bounds.width - seperator*CGFloat(numberOfItems-1))/CGFloat(numberOfItems)
            }
            else
            {
                cellWidth2 = cellWidth
            }


            for row in 0..<numberOfItems {


                let indexPath = NSIndexPath(row: row, section: section)

                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
                attributes.frame = CGRect(x: (cellWidth2+seperator)*CGFloat(row),
                                          y: (cellHeight+seperator)*CGFloat(section),
                                          width: cellWidth2,
                                          height: cellHeight)

                //row_temp.append(attributes)

                self.cache.append(attributes)
            }
            //self.itemAttributes.append(row_temp)
        }

}

override var collectionViewContentSize: CGSize {

    guard let collectionView = collectionView else
    {
        return CGSize.zero
    }

    if (collectionView.numberOfSections <= 0)
    {
        return collectionView.bounds.size
    }

    let width:CGFloat
    if cellWidth <= 0
    {
        width = collectionView.bounds.width
    }
    else
    {
        width = cellWidth*CGFloat(collectionView.numberOfItems(inSection: 0))
    }

    let numberOfSections = CGFloat(collectionView.numberOfSections)
    var height:CGFloat = 0
    height += numberOfSections * cellHeight
    height += (numberOfSections - 1) * seperator



    return CGSize(width: width, height: height)
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    var layoutAttributes = [UICollectionViewLayoutAttributes]()

    for attributes in cache {
        if attributes.frame.intersects(rect) {
            layoutAttributes.append(attributes)
        }
    }
    return layoutAttributes
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return cache[indexPath.item]
}
}
Chen Jiling
fonte
0

Experimente, está funcionando perfeitamente para mim,

Siga as etapas abaixo:

1. Defina os valores no arquivo .h.

     #define kNoOfColumsForCollection 3
     #define kNoOfRowsForCollection 4
     #define kcellSpace 5
     #define kCollectionViewCellWidth (self.view.frame.size.width - kcellSpace*kNoOfColumsForCollection)/kNoOfColumsForCollection
     #define kCollectionViewCellHieght (self.view.frame.size.height-40- kcellSpace*kNoOfRowsForCollection)/kNoOfRowsForCollection

OU

  #define kNoOfColumsForCollection 3
  #define kCollectionViewCellWidthHieght (self.view.frame.size.width - 6*kNoOfColumsForCollection)/kNoOfColumsForCollection

2. Adicionar código na coleção Exibir métodos de fonte de dados de layout como abaixo,

 #pragma mark Collection View Layout data source methods
// collection view with autolayout

- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 4;
}

- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
 {
 return 1;
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(4, 4, 4, 4);
}
- (CGSize)collectionView:(UICollectionView *)collectionView
              layout:(UICollectionViewLayout *)collectionViewLayout
   sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
 return 
 CGSizeMake(kCollectionViewCellWidth,kCollectionViewCellHieght);
 // CGSizeMake (kCollectionViewCellWidthHieght,kCollectionViewCellWidthHieght);
}

Espero que isso ajude alguém.

Jaywant Khedkar
fonte
0

Primeiro adicione UICollectionViewDelegateFlowLayout como protocolo.

Então:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
        var columnCount = 3
        let width  = (view.frame.width - 20) / columnCount
        return CGSize(width: width, height: width)
}
Mahdi Moqadasi
fonte