Home > Software engineering >  Why are the constraints not working when orientation changes and navigation to new view
Why are the constraints not working when orientation changes and navigation to new view

Time:12-24

I am making a collection view that when one of the cells is pressed it navigates to a second view, however I am experiencing a bug. When in Landscape orientation when I navigate to the second view the constraints change for the first view.

This is what it looks like as vertical and landscape when the app is first open,

enter image description here

enter image description here

Now when ever any of these cells are pressed while in LANDSCAPE orientation it takes me to a new view (which is fine), but before the transition finishes, I can see the constraints of the collection view change (which is not) so I go back to the first view and it looks different, it now looks like this:

enter image description here

enter image description here

I am not sure what I am doing wrong. I tried to not use safeAreaLayoutGuide but it makes no difference to this problem.

Here is the code for the ViewController:

class ProfileViewController: UIViewController {

    var profiles: [Profile] = []

    private let collectionView: UICollectionView = {
        let viewLayout = UICollectionViewFlowLayout()
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
        collectionView.backgroundColor = .white
        return collectionView
    }()

    private enum LayoutConstant {
        static let spacing: CGFloat = 16.0
        static let itemHeight: CGFloat = 300.0
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupLayouts()
        populateProfiles()
        collectionView.reloadData()
    }

    private func setupViews() {
        view.backgroundColor = .white
        view.addSubview(collectionView)

        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(ProfileCell.self, forCellWithReuseIdentifier: ProfileCell.identifier)
    }

    private func setupLayouts() {
        collectionView.translatesAutoresizingMaskIntoConstraints = false

        // Layout constraints for `collectionView`
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
            collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
        ])
    }

    private func populateProfiles() {
        profiles = [
            Profile(name: "Thor", location: "Boston", imageName: "", profession: "astronomy"),
            Profile(name: "Mike", location: "Albequerque", imageName: "", profession: "basketball"),
            Profile(name: "Walter White", location: "New Mexico", imageName: "", profession: "chemistry"),
            Profile(name: "Sam Brothers", location: "California", imageName: "", profession: "geography"),
            Profile(name: "Chopin", location: "Norway", imageName: "", profession: "geometry"),
            Profile(name: "Castles", location: "UK", imageName: "", profession: "history"),
            Profile(name: "Dr. Johnson", location: "Australia", imageName: "", profession: "microscope"),
            Profile(name: "Tom Hanks", location: "Bel Air", imageName: "", profession: "theater"),
            Profile(name: "Roger Federer", location: "Switzerland", imageName: "", profession: "trophy"),
            Profile(name: "Elon Musk", location: "San Francisco", imageName: "", profession: "graduate")
        ]
    }

    init() {
        super.init(nibName: nil, bundle: nil)
    }

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

}

extension ProfileViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return profiles.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCell.identifier, for: indexPath) as! ProfileCell

        let profile = profiles[indexPath.row]
        cell.setup(with: profile)
        cell.contentView.backgroundColor = .red
        return cell
    }
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let vc = LightViewController()
    vc.modalPresentationStyle = .fullScreen
    self.present(vc, animated: true)
    var pressedData = Data(id: 1, name: "Test", description: "Test")
    vc.viewModel.lightPublisher.send(pressedData)
  }
}

extension ProfileViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let width = itemWidth(for: view.frame.width, spacing: LayoutConstant.spacing)

        return CGSize(width: width, height: LayoutConstant.itemHeight)
    }

    func itemWidth(for width: CGFloat, spacing: CGFloat) -> CGFloat {
        let itemsInRow: CGFloat = 2

        let totalSpacing: CGFloat = 2 * spacing   (itemsInRow - 1) * spacing
        let finalWidth = (width - totalSpacing) / itemsInRow

        return floor(finalWidth)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: LayoutConstant.spacing, left: LayoutConstant.spacing, bottom: LayoutConstant.spacing, right: LayoutConstant.spacing)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return LayoutConstant.spacing
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return LayoutConstant.spacing
    }
}

this is the code for the cell:

import UIKit

protocol ReusableView: AnyObject {
    static var identifier: String { get }
}

final class ProfileCell: UICollectionViewCell {

    private enum Constants {
        // MARK: contentView layout constants
        static let contentViewCornerRadius: CGFloat = 4.0

        // MARK: profileImageView layout constants
        static let imageHeight: CGFloat = 180.0

        // MARK: Generic layout constants
        static let verticalSpacing: CGFloat = 8.0
        static let horizontalPadding: CGFloat = 16.0
        static let profileDescriptionVerticalPadding: CGFloat = 8.0
    }

    private let profileImageView: UIImageView = {
        let imageView = UIImageView(frame: .zero)
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()

    let name: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        label.numberOfLines = 0
        return label
    }()

    let locationLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        label.numberOfLines = 0
        return label
    }()

    let professionLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.textAlignment = .center
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: .zero)
        setupViews()
        setupLayouts()
    }

    private func setupViews() {
        contentView.clipsToBounds = true
        contentView.layer.cornerRadius = Constants.contentViewCornerRadius
        contentView.backgroundColor = .white

        contentView.addSubview(profileImageView)
        contentView.addSubview(name)
        contentView.addSubview(locationLabel)
        contentView.addSubview(professionLabel)
    }

    private func setupLayouts() {
        profileImageView.translatesAutoresizingMaskIntoConstraints = false
        name.translatesAutoresizingMaskIntoConstraints = false
        locationLabel.translatesAutoresizingMaskIntoConstraints = false
        professionLabel.translatesAutoresizingMaskIntoConstraints = false

        // Layout constraints for `profileImageView`
        NSLayoutConstraint.activate([
            profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            profileImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
            profileImageView.heightAnchor.constraint(equalToConstant: Constants.imageHeight)
        ])

        // Layout constraints for `usernameLabel`
        NSLayoutConstraint.activate([
            name.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.horizontalPadding),
            name.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.horizontalPadding),
            name.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: Constants.profileDescriptionVerticalPadding)
        ])

        // Layout constraints for `descriptionLabel`
        NSLayoutConstraint.activate([
            locationLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.horizontalPadding),
            locationLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.horizontalPadding),
            locationLabel.topAnchor.constraint(equalTo: name.bottomAnchor, constant: 4.0)
        ])

        // Layout constraints for `matchLabel`
        NSLayoutConstraint.activate([
            professionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.horizontalPadding),
            professionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.horizontalPadding),
            professionLabel.topAnchor.constraint(equalTo: locationLabel.bottomAnchor, constant: 8.0),
            professionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.profileDescriptionVerticalPadding)
        ])
    }

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

    func setup(with profile: Profile) {
        profileImageView.image = UIImage(named: profile.imageName)
        name.text = profile.name
        locationLabel.text = profile.location
        professionLabel.text = profile.profession
    }
}


extension ProfileCell: ReusableView {
    static var identifier: String {
        return String(describing: self)
    }
}

If there is anything else I can answer please ask, Thank you


Edit: I took advice from the bottom answers and threw in some combine since I am practicing it. I am able to get the cells to keep the same number, however there is still a layout issue in landscape orientation.

here is what it looks like when moved to landscape when app loads up. To make it easier to see I made the background Color of the view .systemBlue, and the collection view background color is .white.

enter image description here

see the blue at the top. that is what is bothering me, because when I press one of the cells, the collection view seems to move up:

enter image description here

That is the top of the view, it is not a scroll issue.

here is the new view controller:

    class ProfileViewController: UIViewController {
        
     var profiles: [Profile] = []
    @Published var otherSubject = ColumnFlowLayout()
    
      lazy var cancellables = Set<AnyCancellable>()
    
      @Published var testNum: CGFloat = 0
            
               

      private let collectionView: UICollectionView = {
                let viewLayout = UICollectionViewFlowLayout()
                let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
                collectionView.backgroundColor = .white
                return collectionView
            }()
        
            private enum LayoutConstant {
                static let spacing: CGFloat = 16.0
                static let itemHeight: CGFloat = 300.0
            }
        
            override func viewDidLoad() {
                super.viewDidLoad()
                setupViews()
                setupLayouts()
                populateProfiles()
                
**NEW --->      NotificationCenter.default
                .publisher(for: UIDevice.orientationDidChangeNotification)
                .compactMap { _ in UIDevice.current.orientation }
                .sink { value in
                self.collectionView.reloadData()
                if value == .portrait {
                   print("@@@Portrait")
                   self.testNum = 2
                 } else if value == .landscapeLeft || value == .landscapeRight {
                   self.testNum = 4
                   print("@@@Landscape")
                 }
               }
              .store(in: &cancellables)
            }
        
            private func setupViews() {
                view.backgroundColor = .white
                view.addSubview(collectionView)
        
                collectionView.dataSource = self
                collectionView.delegate = self
                collectionView.register(ProfileCell.self, forCellWithReuseIdentifier: ProfileCell.identifier)
            }
        
            private func setupLayouts() {
                collectionView.translatesAutoresizingMaskIntoConstraints = false
        
                // Layout constraints for `collectionView`
                NSLayoutConstraint.activate([
                    collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                    collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
                    collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
                    collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
                ])
            }
        
            private func populateProfiles() {
                profiles = [
                    Profile(name: "Thor", location: "Boston", imageName: "", profession: "astronomy"),
                    Profile(name: "Mike", location: "Albequerque", imageName: "", profession: "basketball"),
                    Profile(name: "Walter White", location: "New Mexico", imageName: "", profession: "chemistry"),
                    Profile(name: "Sam Brothers", location: "California", imageName: "", profession: "geography"),
                    Profile(name: "Chopin", location: "Norway", imageName: "", profession: "geometry"),
                    Profile(name: "Castles", location: "UK", imageName: "", profession: "history"),
                    Profile(name: "Dr. Johnson", location: "Australia", imageName: "", profession: "microscope"),
                    Profile(name: "Tom Hanks", location: "Bel Air", imageName: "", profession: "theater"),
                    Profile(name: "Roger Federer", location: "Switzerland", imageName: "", profession: "trophy"),
                    Profile(name: "Elon Musk", location: "San Francisco", imageName: "", profession: "graduate")
                ]
            }
        
            init() {
                super.init(nibName: nil, bundle: nil)
            }
        
            required init?(coder: NSCoder) {
                fatalError("init(coder:) has not been implemented")
            }
        
        }
        
        extension ProfileViewController: UICollectionViewDataSource {
            func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                return profiles.count
            }
        
            func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProfileCell.identifier, for: indexPath) as! ProfileCell
        
                let profile = profiles[indexPath.row]
                cell.setup(with: profile)
                cell.contentView.backgroundColor = .red
                return cell
            }
          func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            let vc = LightViewController()
            vc.modalPresentationStyle = .fullScreen
            self.present(vc, animated: true)
            var pressedData = Data(id: 1, name: "Test", description: "Test")
            vc.viewModel.lightPublisher.send(pressedData)
          }
        }
        
        extension ProfileViewController: UICollectionViewDelegateFlowLayout {
            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
                let width = itemWidth(for: view.frame.width, spacing: LayoutConstant.spacing)
        
                return CGSize(width: width, height: LayoutConstant.itemHeight)
            }
        
            func itemWidth(for width: CGFloat, spacing: CGFloat) -> CGFloat {
     **NEW ---> let itemsInRow: CGFloat = testNum
        
                let totalSpacing: CGFloat = 2 * spacing   (itemsInRow - 1) * spacing
                let finalWidth = (width - totalSpacing) / itemsInRow
        
                return floor(finalWidth)
            }
        
            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
                return UIEdgeInsets(top: LayoutConstant.spacing, left: LayoutConstant.spacing, bottom: LayoutConstant.spacing, right: LayoutConstant.spacing)
            }
        
            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
                return LayoutConstant.spacing
            }
        
            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
                return LayoutConstant.spacing
            }
        }

I am just wondering what is going on with the top constraint and how do I fix this? Thank you

CodePudding user response:

Your code is setup to draw two cells across the width of the screen. But your code does not attempt to adjust anything during device rotation.

You start the app in portrait and it draws two cells across the width of the screen. Let's say for sake of discussion that results in the cells being 180 points wide.

Now you rotate to landscape. The cell layout is not being updated so the cells stay at 180 points wide. Since the screen is now wider you get 4 cells (roughly) across the screen.

While in landscape you tap a cell and the new screen is presented. As a result of it being presented and then dismissed, the collection view gets redrawn and the layout is recalculated. Since your code specifies two cells across the width, you now have two cells in landscape, just as your code is written to do. So the new cell width is now, let's say, 380 points.

When you now rotate back to portrait those really wide (380 pt) cells are still in use and now only one cell fits (roughly) per row.

So that is why you see the results you see.

You likely need two adjustments to your code:

  1. Update the cell layout as the screen size changes. Do this by overriding the viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) view controller method.
  2. Update your calculations for the cell size as needed to account for different screen sizes.

You should also consider having your ProfileViewController class extend UICollectionViewController instead of UIViewController. If the view controller only has a collection view as its only subview it saves a lot of work and code.

  • Related