UICollectionView Set number of columns

后端 未结 17 1199
执笔经年
执笔经年 2020-11-28 17:28

I just started learning about UICollectionViews. I\'m wondering if anyone knows how to specify the number of columns in a collectionview. The default is set to 3 (iPhone/por

17条回答
  •  醉话见心
    2020-11-28 18:33

    With Swift 5 and iOS 12.3, you can use one the 4 following implementations in order to set the number of items per row in your UICollectionView while managing insets and size changes (including rotation).


    #1. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's itemSize property

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


    #2. Using UICollectionViewFlowLayout's itemSize method

    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. Using UICollectionViewDelegateFlowLayout's collectionView(_:layout:sizeForItemAt:) method

    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. Subclassing UICollectionViewFlowLayout and using UICollectionViewFlowLayout's estimatedItemSize property

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

提交回复
热议问题