UICollectionView Ustaw liczbę kolumn

100

Właśnie zacząłem uczyć się o UICollectionViews. Zastanawiam się, czy ktoś wie, jak określić liczbę kolumn w widoku kolekcji. Wartość domyślna to 3 (iPhone / portret). Przejrzałem dokumentację i nie mogę znaleźć zwięzłej odpowiedzi.

SNV7
źródło
1
Mam aplikację, której używam w UICollectionViewzasadzie jako siatka, ponieważ wiem, że chcę tylko 4 kolumny. Robię to, nadając komórkom określony rozmiar, a odstępy między nimi mają określony rozmiar, aby widok kolekcji wyświetlał tylko 4 kolumny, bez względu na orientację. Czy to jest podobne do tego, czego potrzebujesz?
jhilgert00
A może jest to bardziej ogólne pytanie, na przykład czy mogę dynamicznie ustawić liczbę kolumn, czy nie?
jhilgert00
Zasadniczo chcę mieć 7 kolumn w trybie portretowym. Więc ustawiłem szerokość kolumn na 46px (320 px / 46px = 7). Nie wiedziałem, że jest to właściwość odstępu.
SNV7

Odpowiedzi:

81

CollectionViews są bardzo wydajne i mają swoją cenę. Wiele, wiele opcji. Jak powiedział omz:

istnieje wiele sposobów zmiany liczby kolumn

Proponuję zaimplementować <UICollectionViewDelegateFlowLayout>protokół, dający dostęp do następujących metod, dzięki którym możesz mieć większą kontrolę nad układem swojego UICollectionView, bez potrzeby tworzenia podklasy:

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

Ponadto zaimplementowanie poniższej metody zmusi UICollectionView do zaktualizowania układu przy zmianie orientacji: (powiedzmy, że chcesz zmienić rozmiar komórek na poziomy i rozciągnąć)

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

    [self.myCollectionView.collectionViewLayout invalidateLayout];
}

Dodatkowo tutaj są 2 naprawdę dobre samouczki na temat UICollectionViews:

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

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

jhilgert00
źródło
1
Czy to zadziała na wszystkich urządzeniach z iOS? Czy collectionView:layout:sizeForItemAtIndexPath:należy obsługiwać oddzielnie wszystkie urządzenia?
Sharath
1
@Sharath, oczywiście, działa na wszystko. Wystarczy, że wykonasz obliczenia w sposób dostosowany do wszystkich urządzeń;). Projekt adaptacyjny.
TheCodingArt
wow taki wspaniały, ale prawdziwy przykład? w Androidzie możemy to zrobić new GridLayoutManager(mContext, 4))(4 kolumny), bardzo łatwo lub po prostu użyć LinearLayoutManager(ma oczywiście tylko 1 kolumnę)
użytkownik25
145

W Swift 5 i iOS 12.3 możesz użyć jednej z 4 następujących implementacji , aby ustawić liczbę elementów w wierszu UICollectionViewpodczas zarządzania wstawkami i zmianami rozmiaru (w tym rotacją).


# 1. Instacji UICollectionViewFlowLayouti przy użyciu UICollectionViewFlowLayout„s itemSizewłaściwość

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
    }

}

wprowadź opis obrazu tutaj


# 2. Korzystanie UICollectionViewFlowLayoutz itemSizemetody

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. Korzystanie UICollectionViewDelegateFlowLayoutz collectionView(_:layout:sizeForItemAt:)metody

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. Instacji UICollectionViewFlowLayouti przy użyciu UICollectionViewFlowLayout„s estimatedItemSizewłaściwość

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

}

wprowadź opis obrazu tutaj

Imanou Petit
źródło
3
Rozwiązania korzystające z parametru „viewWillTransition (to size…”) nie obsługują prawidłowo obracania urządzenia. Ponieważ jest on wywoływany przed ustawieniem ramki, do obliczania rozmiaru komórki jest używany stary wymiar collectionView. (Nie próbowałem jeszcze inne rozwiązania)
vale
2
Skorzystałem z rozwiązania nr 1, jesteś moim nowym bohaterem.
Tom Wolters
1
Skorzystałem z twojego rozwiązania nr 1. Jest wspaniały.
shahil,
1
świetna i kompletna odpowiedź, wdrożona # 2
Led
1
Rozwiązanie nr 1 jest idealne! Dziękuję za wkład!
Brian,
61

Zaimplementowałem UICollectionViewDelegateFlowLayoutna swoim UICollectionViewControlleri nadpisałem metodę odpowiedzialną za określenie rozmiaru komórki. Następnie wziąłem szerokość ekranu i podzieliłem ją na wymagania dotyczące kolumny. Na przykład chciałem mieć 3 kolumny na każdym rozmiarze ekranu. Oto jak wygląda mój kod -

- (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;
}
Nowicjusz
źródło
2
O wiele prostsze, dziękuję za rozsądek i mniej kodu
jjxtra
2
Powinieneś upewnić się, że odstępy między elementami są ustawione na 0, aby uniknąć zamiast tego n-1 mocno rozstawionych elementów na wiersz.
Joe
41

Rozwinięcie odpowiedzi Nooba:

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

Pozwala to na dowolne odstępy między komórkami. Zakłada Intzmienną składową o nazwie, numberOfItemsPerRowa także, że wszystkie komórki są kwadratowe i mają ten sam rozmiar. Jak zauważono w odpowiedzi jhilgert00, musimy również reagować na zmiany orientacji, ale teraz używamy wartości, viewWillTransitionToSizektóra willRotateToInterfaceOrientationjest amortyzowana.

sgib
źródło
let size = Int ((collectionView.bounds.width - totalSpace) / CGFloat (numberOfItemsPerRow)) - należy usunąć rzutowanie na Int, ponieważ w niektórych przypadkach tworzy małe odstępy między komórkami
Makalele
14

Oto działający kod dla Swift 3, aby mieć układ dwóch kolumn:

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

Możesz zmienić „nbCol” na żądaną liczbę kolumn.

fraxool
źródło
co jest nie tak z każdym rozwiązaniem, gdy wstawki są (1.0,1.0,1.0,1.0), układ staje się w tym przypadku 1 kolumną!
rd_
@fraxool Świetna odpowiedź, ale jeśli odwrócę urządzenie, układ się zresetuje ... Jakieś rozwiązanie?
Maksim Kniazev
8

Jeśli jesteś leniwy używając delegate.

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: Należy również wywołać po rotacji

HotJard
źródło
2
Działa również w Swift 4.0 ... po zmianach ostatnie dwie linie: let width = floor ((frame.width - insetSpace - innerSpace) / itemsInRow) layout.itemSize = CGSize (width: width, height: width)
g212gs
8

Zaktualizowano do Swift 3:

Zamiast układu przepływu wolę używać układu niestandardowego dla określonego numeru kolumny i numeru wiersza. Ponieważ:

  1. Można go przeciągać poziomo, jeśli numer kolumny jest bardzo duży.
  2. Jest to bardziej akceptowalne ze względu na użycie kolumny i wiersza.

Normalna komórka i komórka nagłówka: (Dodaj UILabel jako IBOutlet do swojego 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
}
}

Układ niestandardowy:

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

Użyj CollectionView z 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()
}
}

Wreszcie możesz mieć fantazyjny CollectionView!

wprowadź opis obrazu tutaj

brianLikeApple
źródło
problem z przyjęciem tego rozwiązania to statyczna wysokość i szerokość
rd_
2
@rd_Custom layout oznacza, że ​​możesz robić, co tylko zechcesz! Mój kod pokazuje tylko najprostszy przypadek ......
brianLikeApple
7

Ponieważ UICollectionView jest tak elastyczny, istnieje wiele sposobów zmiany liczby kolumn w zależności od rodzaju używanego układu.

UICollectionViewFlowLayout(Co jest chyba to, co pracujesz z) nie określa liczbę kolumn bezpośrednio (ponieważ zależy rozmiar / orientację). Najłatwiejszym sposobem zmiany byłoby ustawienie itemSizewłaściwości i / lub minimumInteritemSpacing/ minimumLineSpacing.

omz
źródło
4

Swift 3.0. Działa dla kierunków przewijania w poziomie i pionie oraz dla zmiennych odstępów

Określ liczbę kolumn

let numberOfColumns: CGFloat = 3

Skonfiguruj, flowLayoutaby renderować określonenumberOfColumns

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
źródło
Wszystkie te długie odpowiedzi są wszędzie i to działa z 5 liniami kodu :)
J. Doe,
1
Dobra próba. Ale to nie zadziała, jeśli użyjesz sekcji sectionInset w collectionViewLayout. Aby to naprawić, (1) oblicz dostępną szerokość, biorąc szerokość komórki i odejmując od niej parametry flowLayout.sectionInset.left i flowLayout.sectionInset.right. (2) Kiedy obliczasz cellWidth, zamień „view.frame.width” na wartość obliczoną w (1). Pamiętaj też, że ramka widoku zostanie sfinalizowana dopiero po załadowaniu i może ulec zmianie, więc w razie potrzeby będziesz musiał przebudować układ.
Womble
4

Zaktualizowano do Swift 5+ iOS 13

Szacunkowy rozmiar widoku kolekcji nie może wynosić zero

wprowadź opis obrazu tutaj

Zadeklaruj margines dla komórki

let margin: CGFloat = 10

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

Metoda 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
źródło
2

Chodzi o układ, który chcesz narysować. Możesz utworzyć niestandardową klasę dziedziczącą po UICollectionViewFlowLayout. Obecnie nie ma żadnej bezpośredniej metody ustawiania kolumn. Jeśli chcesz osiągnąć taką funkcjonalność, musisz to zrobić ręcznie. Musisz obsłużyć to w niestandardowej klasie układu przepływu.

Teraz pojawi się pytanie, jak to zrobisz? Jeśli nie chcesz zakłócać ramki komórki, możesz ją dostosować

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

Innym sposobem jest podanie własnych pozycji komórek. Zastępując poniżej dwie metody, które zostaną wywołane podczas tworzenia układu.

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

UICollectionViewLayoutAttributes to klasa, która zajmuje się pozycją komórki, ramką, Zindex itp

Swapnil
źródło
2

Ta odpowiedź dotyczy swift 3.0 ->

najpierw zgodne z protokołem:

 UICollectionViewDelegateFlowLayout

następnie dodaj te metody:

    //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
źródło
1

Chciałem tylko dołączyć do odpowiedzi Imanou Petit # 2. Aby upewnić się, że marginesy są dokładne niezależnie od szerokości ekranu, używam solwera iteracyjnego z żądanym marginesem i liczbą kolumn jako danymi wejściowymi. Dodałem również flagę kierunkową wskazującą, gdzie ich końcowe marginesy będą porównywane z celem.

Iteracyjny solver jest pokazany poniżej i zwraca cellWidth i 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)
}

Ja to tak nazywam:

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

Następnie stosuję wartości cellWidth i margin do układu przepływu jako takie:

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
}

}

Mam nadzieję że to pomoże. Prawdopodobnie istnieje łatwiejszy sposób na upewnienie się, że marginesy są dokładne, ale jest to jedna z metod. Ponadto ten kod nie został przetestowany pod kątem urządzeń, które umożliwiają obracanie widoków kolekcji.

Dzięki,

Jan-Michael Tressler
źródło
0

idealnym rozwiązaniem jest użycie UICollectionViewDelegateFlowLayout, ale możesz łatwo obliczyć szerokość komórki i podzielić ją na żądaną liczbę kolumn

trudne jest zrobienie szerokości bez ułamka

(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 Angry
źródło
0

Zrobiłem układ kolekcji.

Aby separator był widoczny, ustaw kolor tła widoku kolekcji na szary. Jeden rząd na sekcję.

Zastosowanie:

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

Układ:

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
źródło
0

Spróbuj tego, działa idealnie dla mnie,

Wykonaj poniższe kroki:

1. Zdefiniuj wartości w pliku .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

LUB

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

2. Dodaj kod do kolekcji Zobacz metody źródła danych Układ, jak poniżej,

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

Mam nadzieję, że komuś to pomoże.

Jaywant Khedkar
źródło
0

Najpierw dodaj UICollectionViewDelegateFlowLayout jako protokół.

Następnie:

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
źródło