How can I center rows in UICollectionView?

前端 未结 9 1713
囚心锁ツ
囚心锁ツ 2021-02-02 06:24

I have a UICollectionView with random cells. Is there any method that allows me to center rows?

This is how it looks by default:

[ x x x x         


        
9条回答
  •  误落风尘
    2021-02-02 07:06

    With Swift 4.1 and iOS 11, according to your needs, you may choose one of the 2 following complete implementations in order to fix your problem.


    #1. Center UICollectionViewCells with fixed size

    The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:) and UICollectionViewFlowLayout's itemSize in order to center the cells of a UICollectionView:

    CollectionViewController.swift

    import UIKit
    
    class CollectionViewController: UICollectionViewController {
    
        let columnLayout = FlowLayout(
            itemSize: CGSize(width: 140, height: 140),
            minimumInteritemSpacing: 10,
            minimumLineSpacing: 10,
            sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        )
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            title = "Center cells"
    
            collectionView?.collectionViewLayout = columnLayout
            collectionView?.contentInsetAdjustmentBehavior = .always
            collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        }
    
        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 7
        }
    
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
            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 {
    
        required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
            super.init()
    
            self.itemSize = itemSize
            self.minimumInteritemSpacing = minimumInteritemSpacing
            self.minimumLineSpacing = minimumLineSpacing
            self.sectionInset = sectionInset
            sectionInsetReference = .fromSafeArea
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
            guard scrollDirection == .vertical else { return layoutAttributes }
    
            // Filter attributes to compute only cell attributes
            let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
    
            // Group cell attributes by row (cells with same vertical center) and loop on those groups
            for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
                // Get the total width of the cells on the same row
                let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                    partialWidth + attribute.size.width
                }
    
                // Calculate the initial left inset
                let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
                var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
    
                // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
                for attribute in attributes {
                    attribute.frame.origin.x = leftInset
                    leftInset = attribute.frame.maxX + minimumInteritemSpacing
                }
            }
    
            return layoutAttributes
        }
    
    }
    

    CollectionViewCell.swift

    import UIKit
    
    class CollectionViewCell: UICollectionViewCell {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            contentView.backgroundColor = .cyan
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    

    Expected result:


    #2. Center autoresizing UICollectionViewCells

    The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout's estimatedItemSize and UILabel's preferredMaxLayoutWidth in order to center the cells of a UICollectionView:

    CollectionViewController.swift

    import UIKit
    
    class CollectionViewController: UICollectionViewController {
    
        let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]
    
        let columnLayout = FlowLayout(
            minimumInteritemSpacing: 10,
            minimumLineSpacing: 10,
            sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        )
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            title = "Center cells"
    
            collectionView?.collectionViewLayout = columnLayout
            collectionView?.contentInsetAdjustmentBehavior = .always
            collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        }
    
        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return array.count
        }
    
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
            cell.label.text = array[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 {
    
        required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
            super.init()
    
            estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
            self.minimumInteritemSpacing = minimumInteritemSpacing
            self.minimumLineSpacing = minimumLineSpacing
            self.sectionInset = sectionInset
            sectionInsetReference = .fromSafeArea
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
            guard scrollDirection == .vertical else { return layoutAttributes }
    
            // Filter attributes to compute only cell attributes
            let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
    
            // Group cell attributes by row (cells with same vertical center) and loop on those groups
            for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
                // Get the total width of the cells on the same row
                let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                    partialWidth + attribute.size.width
                }
    
                // Calculate the initial left inset
                let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
                var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
    
                // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
                for attribute in attributes {
                    attribute.frame.origin.x = leftInset
                    leftInset = attribute.frame.maxX + minimumInteritemSpacing
                }
            }
    
            return layoutAttributes
        }
    
    }
    

    CollectionViewCell.swift

    import UIKit
    
    class CollectionViewCell: UICollectionViewCell {
    
        let label = UILabel()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            contentView.backgroundColor = .orange
            label.preferredMaxLayoutWidth = 120
            label.numberOfLines = 0
    
            contentView.addSubview(label)
            label.translatesAutoresizingMaskIntoConstraints = false
            contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
            contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
            contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
            contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    

    Expected result:


提交回复
热议问题