swiftuicollectionviewuikituicollectionviewcell

Center a UIView without affecting the position of other UI classes in UIKit programmatically


I want to move the position of a UIView boardGame: UICollectionView to be on the center of the screen by modifying the constraints properties such as topAnchor, but anything I do to it seems to affect the content inside of it. I have also defined a UICollectionViewCell model so I can be register the cells in boardGame as "uinique".

import UIKit

class BoardViewController: UIViewController {

    private let boardGame: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let board = UICollectionView(frame: .zero, collectionViewLayout: layout)
        board.allowsSelection =  true
        board.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: "unique")
        board.translatesAutoresizingMaskIntoConstraints = false
        return board
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(boardGame)
        boardGame.backgroundColor = .systemGray2
        
        boardGame.frame = view.bounds
        
        let margin: CGFloat = 20
        NSLayoutConstraint.activate([
               
            boardGame.topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
            boardGame.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                   boardGame.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
                  
                   boardGame.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
                   boardGame.heightAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
               ])
        
       
        
        boardGame.delegate = self
        boardGame.dataSource = self
        
    }
    
    init() {
           super.init(nibName: nil, bundle: nil)
       }
       required init?(coder: NSCoder) {
           fatalError("init(coder:) has not been implemented")
       }
}

extension BoardViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
    
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 9
    }
    
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "unique", for: indexPath) as! CustomCollectionViewCell

              
              let chessRow = indexPath.row / 3
              if chessRow % 2 == 0 {
                  if indexPath.row % 2 == 0 {
                       cell.backgroundColor = UIColor.white
                  }else{
                      cell.backgroundColor = UIColor.black
                  }
              } else{
                  if indexPath.row % 2 == 0 {
                      cell.backgroundColor = UIColor.black
                  }else{
                      cell.backgroundColor = UIColor.white
                  }
              }
          
              return cell
          }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            // Divide the width by 3 to create a 3x3 grid
            let width = collectionView.frame.size.width / 4.0
            let height = width
            return CGSize(width: width, height: height)
        }
    
}

Here is the CustomCollectionViewCell: UICollectionViewCell model:

class CustomCollectionViewCell: UICollectionViewCell {
   
    let titleLabel = UILabel()

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

        contentView.addSubview(titleLabel)

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

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

And the parent view that receives the instance of the BoardViewController

import UIKit

class MainViewController: UIViewController {

    let boardView = BoardViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .blue
        addBoardView()
    }
    
    func addBoardView(){
        addChild(boardView)
        view.addSubview(boardView.view)
        
     
        boardView.didMove(toParent: self)
    }
}

I have tried playing around with the properties of the constant: CGFloat in topAnchor in boardGame. To give some context here is an example of what I get with boardGame.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),

enter image description here

If I change the CGFloat to 200 to make it more centered the properties of the CustomCollectionViewCell inside it moved to the edged afecting the top margin like so:

enter image description here

I already tried to mess with the titleLabel top margin constraint inside of CustomCollectionViewCell but does not seem to do anything.

How could I keep the boardGame view in the center without affecting the margin of CustomCollectionViewCell?


Solution

  • The cells are positioned at the top edge of the collection view, because that's how collection views work. This is by design. The cells only appear "centred", when the collection view is at the top of the screen, because the cells are laid out to avoid the safe area (the dynamic island in this case) by default.

    In any case, your constraints are contradictory. You are saying that the boardGame view should have a fixed width and height (assuming the superview's width and height are fixed), and its centre point should be the same as the superview's centre point (centerX = view.centerX and centerY = view.centerY), and its top is only 20 points away from the superview's top.

    These cannot all be satisfied simultaneously. As you can see in the first screenshot, the centerY = view.centerY constraint is broken, to satisfy the other constraints.

    If you want the collection view to be centred both vertically and horizontally, just remove the top margin constraint:

    boardGame.topAnchor.constraint(equalTo: view.topAnchor, constant: margin)
    

    If you also want the cells to be centred vertically, you can set contentOffset:

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        boardGame.contentOffset.y = (boardGame.contentSize.height - boardGame.bounds.height) / 2
    }
    

    That said, if boardGame is not supposed to be scrollable, consider using a series of UIStackViews for this layout instead. Example:

    class BoardViewController: UIViewController {
        
        var boardGame: UIView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            func makeCell(color: UIColor) -> Cell {
                let cell = Cell(frame: .zero)
                cell.backgroundColor = color
                return cell
            }
            
            func makeColumn(_ views: [UIView]) -> UIStackView {
                let col = UIStackView(arrangedSubviews: views)
                col.axis = .vertical
                col.spacing = 8
                col.alignment = .center
                col.distribution = .fillEqually
                return col
            }
            let col1 = makeColumn([makeCell(color: .black), makeCell(color: .black), makeCell(color: .black)])
            let col2 = makeColumn([makeCell(color: .white), makeCell(color: .white), makeCell(color: .white)])
            let col3 = makeColumn([makeCell(color: .black), makeCell(color: .black), makeCell(color: .black)])
            let columns = UIStackView(arrangedSubviews: [col1, col2, col3])
            columns.axis = .horizontal
            columns.spacing = 16
            columns.distribution = .fillEqually
            columns.translatesAutoresizingMaskIntoConstraints = false
            
            boardGame = UIView()
            boardGame.backgroundColor = .gray
            boardGame.addSubview(columns)
            boardGame.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(boardGame)
            NSLayoutConstraint.activate([
                columns.topAnchor.constraint(equalTo: boardGame.topAnchor, constant: 20),
                columns.bottomAnchor.constraint(equalTo: boardGame.bottomAnchor, constant: -20),
                columns.leftAnchor.constraint(equalTo: boardGame.leftAnchor, constant: 8),
                columns.rightAnchor.constraint(equalTo: boardGame.rightAnchor, constant: -8),
                boardGame.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                boardGame.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                
                
                boardGame.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
                boardGame.heightAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
            ])
        }
        
    }
    
    class Cell: UIView {
        
        let titleLabel = UILabel()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            
            addSubview(titleLabel)
            
            titleLabel.translatesAutoresizingMaskIntoConstraints = false
            titleLabel.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
            titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
            titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
            titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
            
            self.translatesAutoresizingMaskIntoConstraints = false
            self.widthAnchor.constraint(equalTo: self.heightAnchor).isActive = true
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }