iosswiftuicollectionviewuicollectionviewflowlayout

Swift CollectionViewCell has different frame than its CollectionView


The Setup:
I have a CollectionView which has fills the whole screen, has horizontal scrollDirection and is paging enabled. It is inside of a NavigationController with a hidden navigationBar. The CollectionViewCells have the height of the screen too.

What I would expect to happen:
Each cell fills the whole screen and when I swipe to left/right another cell appears and fills the whole screen.
As you can see here the blue border is the border of the collectionView and the red border is the border of the cell. The blue border is how I expect it to be but I would expect the red border to be like the blue border.

What actually happens:
The cells are not filling the whole screen. There is a little space (approx. 20 pxl) between the top of the cells and the top border of the screen. Also the bottom of the cell (also approx. 20 pxl) is "under" the bottom border of the screen.
Also i get a warning:

The behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.

So the cells height is correct as it seems, but the y-value is wrong.

What I already tried:
override shouldInvalidateLayoutForBoundsChange to return true.

Here is the code:

class LoginController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, LoginControllerDelegate, UIGestureRecognizerDelegate {

    lazy var loginCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal 
        layout.minimumLineSpacing = 0
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.dataSource = self
        cv.delegate = self
        cv.isPagingEnabled = true
        cv.layer.borderWidth = 1.0
        cv.layer.borderColor = UIColor.blue.cgColor
        return cv
    }()
    
    let introCellId = "IntroCellId"
    let loginCellId = "LoginCellId"
    
    let pages: [Page] = {
        let firstPage = Page(title: String,
                             message: String,
                             imageName: String)
        
        let secondPage = Page(title: String,
                             message: String,
                             imageName: String)
        
        let thirdPage = Page(title: String,
                             message: String,
                             imageName: String)
        
        return [firstPage, secondPage, thirdPage]
    }()
    
    override func viewDidLoad() {        
        super.viewDidLoad()

        navigationController?.navigationBar.isHidden = true

        view.addSubview(loginCollectionView)
        
        registerCells()
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return pages.count + 1
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        if indexPath.item == pages.count {
            let loginCell = collectionView.dequeueReusableCell(withReuseIdentifier: loginCellId, for: indexPath) as! LoginCell
            loginCell.loginControllerDelegate = self
            loginCell.layer.borderColor = UIColor.red.cgColor
            loginCell.layer.borderWidth = 2.0
            return loginCell
        }

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: introCellId, for: indexPath) as! PageCell
        let page = pages[indexPath.item]
        cell.page = page
        cell.layer.borderColor = UIColor.red.cgColor
        cell.layer.borderWidth = 2.0
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: self.loginCollectionView.frame.width, height: self.loginCollectionView.frame.height)
    }
}

PageCell:

class PageCell: UICollectionViewCell {
    
    var page: Page? {
        didSet {
            guard let page = page
            else {
                return
            }
            
            imageView.image = UIImage(named: page.imageName)
            
            let color = UIColor(white: 0.2, alpha: 1)
            
            let attributedText = NSMutableAttributedString(string: page.title, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: UIFont.Weight.medium), NSAttributedString.Key.foregroundColor: color])
            
            attributedText.append(NSMutableAttributedString(string: "\n\n\(page.message)", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15, weight: UIFont.Weight.medium), NSAttributedString.Key.foregroundColor: color]))
            
            let paragraphStyle = NSMutableParagraphStyle()
            
            paragraphStyle.alignment = .center
            
            let length = attributedText.string.count
            
            attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: length))
            
            textView.attributedText = attributedText
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupViews()
    }
    
    let imageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.backgroundColor = UIColor(displayP3Red: 139/255, green: 221/255, blue: 116/255, alpha: 1)
        iv.clipsToBounds = true
        return iv
    }()
    
    let textView: UITextView = {
        let tv = UITextView()
        tv.isEditable = false
        tv.contentInset = UIEdgeInsets(top: 24, left: 0, bottom: 0, right: 0)
        tv.textColor = UIColor(displayP3Red: 51/255, green: 51/255, blue: 51/255, alpha: 1)
        return tv
    }()
    
    func setupViews() {
        
        addSubview(imageView)
        addSubview(textView)
        
        _ = imageView.anchor(topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: screenWidth, heightConstant: (screenHeight) / 2)

        _ = textView.anchor(imageView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: screenWidth, heightConstant: (screenHeight) / 2)
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been impremented")
    }
}

LoginCell:

class LoginCell: UICollectionViewCell {
    
    let logoImageView: UIImageView = {
        let image = UIImage(named: "logo green")
        let imageView = UIImageView(image: image)
        return imageView
    }()
    
    let emailTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "E-Mail"
        textField.layer.borderColor = UIColor.lightGray.cgColor
        let border = CALayer()
        border.frame = CGRect(x: 0, y: 40, width: UIScreen.main.bounds.width - 64, height: 2.0)
        border.backgroundColor = UIColor(displayP3Red: 51/255, green: 51/255, blue: 51/255, alpha: 1).cgColor
        textField.layer.addSublayer(border)
        textField.keyboardType = .emailAddress
        return textField
    }()
    
    let emailAlertLabel: UILabel = {
        let label = UILabel()
        label.text = "E-Mail-Adresse nicht gefunden"
        label.textColor = .red
        label.isHidden = true
        return label
    }()
    
    let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Passwort"
        textField.layer.borderColor = UIColor.lightGray.cgColor
        let border = CALayer()
        border.frame = CGRect(x: 0, y: 40, width: UIScreen.main.bounds.width - 64, height: 2.0)
        border.backgroundColor = UIColor(displayP3Red: 51/255, green: 51/255, blue: 51/255, alpha: 1).cgColor
        textField.layer.addSublayer(border)
        textField.isSecureTextEntry = true
        return textField
    }()
    
    let passwordAlertLabel: UILabel = {
        let label = UILabel()
        label.text = "Ungültiges Passwort"
        label.textColor = .red
        label.isHidden = true
        return label
    }()
    
    lazy var forgotPasswordButton: UIButton = {
        let button = UIButton(type: .system)
        button.backgroundColor = .white
        button.setTitle("Passwort vergessen?", for: .normal)
        button.setTitleColor(UIColor(displayP3Red: 139/255, green: 221/255, blue: 116/255, alpha: 1), for: .normal)
        button.addTarget(self, action: #selector(handleForgotPassword), for: .touchUpInside)
        button.contentHorizontalAlignment = .left
        return button
    }()
    
    lazy var loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.backgroundColor = UIColor(displayP3Red: 139/255, green: 221/255, blue: 116/255, alpha: 1)
        button.setTitle("Login", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.addTarget(self, action: #selector(checkUserData), for: .touchUpInside)
        button.layer.cornerRadius = 25.0
        return button
    }()
    
    weak var loginControllerDelegate: LoginControllerDelegate?
    
    lazy var stayLoggedInSwitch: UISwitch = {
        let loginSwitch = UISwitch(frame: CGRect(x: 150, y: 150, width: 0, height: 0))
        loginSwitch.addTarget(self, action: #selector(LoginCell.handleStayLoggedInState(_:)), for: .valueChanged)
        loginSwitch.setOn(false, animated: true)
        return loginSwitch
    }()
    
    let stayLoggedInTextField: UITextField = {
        let textField = UITextField()
        textField.text = "Angemeldet bleiben"
        textField.textColor = UIColor(displayP3Red: 51/255, green: 51/255, blue: 51/255, alpha: 1)
        textField.isUserInteractionEnabled = false
        return textField
    }()
    
    let registerLabel: UILabel = {
        let label = UILabel()
        label.text = "Sie haben noch keinen Account?"
        label.textColor = .lightGray
        label.backgroundColor = .white
        label.font = label.font.withSize(13)
        return label
    }()
    
    lazy var registerButton: UIButton = {
        let button = UIButton(type: .system)
        button.backgroundColor = .white
        button.setTitle("Registrieren", for: .normal)
        button.setTitleColor(UIColor(displayP3Red: 139/255, green: 221/255, blue: 116/255, alpha: 1), for: .normal)
        button.addTarget(self, action: #selector(handleRegister), for: .touchUpInside)
        button.contentHorizontalAlignment = .left
        button.titleLabel?.font = button.titleLabel?.font.withSize(13)
        return button
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setupViews()
    }
    
    func setupViews() {
        
        addSubview(logoImageView)
        addSubview(emailTextField)
        addSubview(emailAlertLabel)
        addSubview(passwordTextField)
        addSubview(passwordAlertLabel)
        addSubview(forgotPasswordButton)
        addSubview(loginButton)
        addSubview(stayLoggedInSwitch)
        addSubview(stayLoggedInTextField)
        addSubview(registerLabel)
        addSubview(registerButton)
        
        _ = logoImageView.anchor(centerYAnchor, left: nil, bottom: nil, right: nil, topConstant: -230, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 160, heightConstant: 160)
        logoImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        
        _ = emailTextField.anchor(logoImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 35, leftConstant: 32, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 50)
        
        _ = emailAlertLabel.anchor(emailTextField.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 0, leftConstant: 32, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 20)
        
        _ = passwordTextField.anchor(emailAlertLabel.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 0, leftConstant: 32, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 50)
        
        _ = passwordAlertLabel.anchor(passwordTextField.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 0, leftConstant: 32, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 20)
        
        _ = forgotPasswordButton.anchor(passwordAlertLabel.bottomAnchor, left: leftAnchor, bottom: nil, right: nil, topConstant: 0, leftConstant: 32, bottomConstant: 0, rightConstant: 0, widthConstant: 200, heightConstant: 25)
        
        _ = loginButton.anchor(forgotPasswordButton.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, topConstant: 4, leftConstant: 32, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 50)
        
        _ = stayLoggedInSwitch.anchor(loginButton.bottomAnchor, left: leftAnchor, bottom: nil, right: nil, topConstant: 16, leftConstant: 32, bottomConstant: 0, rightConstant: 0, widthConstant: 60, heightConstant: 50)
        
        _ = stayLoggedInTextField.anchor(loginButton.bottomAnchor, left: stayLoggedInSwitch.rightAnchor, bottom: nil, right: rightAnchor, topConstant: 16, leftConstant: 0, bottomConstant: 0, rightConstant: 32, widthConstant: 0, heightConstant: 32)
        
        _ = registerLabel.anchor(nil, left: leftAnchor, bottom: bottomAnchor, right: nil, topConstant: 0, leftConstant: 32, bottomConstant: 32, rightConstant: 0, widthConstant: 200, heightConstant: 25)
        
        _ = registerButton.anchor(nil, left: registerLabel.rightAnchor, bottom: bottomAnchor, right: rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 32, rightConstant: 32, widthConstant: 0, heightConstant: 25)
        
        emailTextField.addTarget(self, action: #selector(underlineTextFieldColor(sender:)), for: .editingDidBegin)
        emailTextField.addTarget(self, action: #selector(underlineTextFieldDark(sender:)), for: .editingDidEnd)
        passwordTextField.addTarget(self, action: #selector(underlineTextFieldColor(sender:)), for: .editingDidBegin)
        passwordTextField.addTarget(self, action: #selector(underlineTextFieldDark(sender:)), for: .editingDidEnd)
    }
    
    @objc func underlineTextFieldDark(sender: UITextField) {
        sender.layer.sublayers![0].backgroundColor = UIColor(displayP3Red: 51/255, green: 51/255, blue: 51/255, alpha: 1).cgColor
    }
    
    @objc func underlineTextFieldColor(sender: UITextField) {
        sender.layer.sublayers![0].backgroundColor = UIColor(displayP3Red: 139/255, green: 221/255, blue: 116/255, alpha: 1).cgColor
    }
    
    @objc func handleLogin() {
        loginControllerDelegate?.finishLoggingIn()
    }
    
    @objc func handleStayLoggedInState(_ sender: UISwitch) {
        if (sender.isOn == true) {
            UserDefaults.standard.setIsLoggedIn(value: true)
        }
        else {
            UserDefaults.standard.setIsLoggedIn(value: false)
        }
    }
    
    @objc func handleForgotPassword() {
        loginControllerDelegate?.finishLoggingIn()
    }
    
    @objc func handleRegister() {
        loginControllerDelegate?.finishLoggingIn()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

The Anchor method:

extension UIView {

    func anchor(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
        translatesAutoresizingMaskIntoConstraints = false

        var anchors = [NSLayoutConstraint]()

        if let top = top {
            anchors.append(topAnchor.constraint(equalTo: top, constant: topConstant))
        }

        if let left = left {
            anchors.append(leftAnchor.constraint(equalTo: left, constant: leftConstant))
        }

        if let bottom = bottom {
            anchors.append(bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant))
        }

        if let right = right {
            anchors.append(rightAnchor.constraint(equalTo: right, constant: -rightConstant))
        }

        if widthConstant > 0 {
            anchors.append(widthAnchor.constraint(equalToConstant: widthConstant))
        }

        if heightConstant > 0 {
            anchors.append(heightAnchor.constraint(equalToConstant: heightConstant))
        }

        anchors.forEach({$0.isActive = true})

        return anchors
    }
}

Solution

  • The collection view automatically set to the whole view. The contents of the collectionView is able to adjust the insects automatically if you do not set any behavior. At this case set automatic adjustment false to get the full-screen view. The top and bottom of the screen have a safe area for adjusting view with the notch.

    Try this

    collectionView?.contentInsetAdjustmentBehavior = .never
    

    Hope this will fix this issue.