Search code examples
iosswiftuiviewuiviewcontrolleraddsubview

Setup the constrains between the UIView and UIViewController


I want to add view controller as subview inside the UIView . I have given the required constrains but I am not figure it out why it overlapping . Here is minimal code for it. I have given the top , bottom , leading , trailing constrains but not sure why it overlaps . I want to put svc below the parentView

  final class MovieDetailsDisplayViewController: UIViewController {
    
    let movieDetails: MovieDetails
    let viewModel: MoviesDetailsViewModel

    init(movieDetails: MovieDetails, viewModel: MoviesDetailsViewModel) {
        self.movieDetails = movieDetails
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        view = parentView()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //let parentView = parentView()
        (view as? parentView)?.configure(movieDetails: movieDetails)
        
       childView()
        
    }
    
    private func childView() {
        let addHereView = UIView()
        addHereView.backgroundColor = .clear
        addHereView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(addHereView)
        let similarVC = SmiliarMovieViewController(viewModel: viewModel)
        addChild(similarVC)
        
        // safely unwrap MySimilarViewController's view
        guard let similarView = similarVC.view else { return }
        
        similarView.translatesAutoresizingMaskIntoConstraints = false
        addHereView.addSubview(similarView)
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            addHereView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            addHereView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            addHereView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
            // height of the child content
            addHereView.heightAnchor.constraint(equalToConstant: 170.0),
            
            similarView.topAnchor.constraint(equalTo: addHereView.topAnchor),
            similarView.leadingAnchor.constraint(equalTo: addHereView.leadingAnchor),
            similarView.trailingAnchor.constraint(equalTo: addHereView.trailingAnchor),
            similarView.bottomAnchor.constraint(equalTo: addHereView.bottomAnchor),
        ])
        
        similarVC.didMove(toParent: self)
    }
    
    private class parentView: UIView {
        let scrollView = UIScrollView()
        let backdropImageView = UIImageView()
        let titleLabel = UILabel()
        let overviewLabel = UILabel()
        let addHereView = UIView()

                
        private lazy var contentStackView = UIStackView(arrangedSubviews: [backdropImageView, titleLabel, overviewLabel])
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }

        private func commonInit() {
            backgroundColor = .white
            addHereView.backgroundColor = .systemBlue
            addHereView.translatesAutoresizingMaskIntoConstraints = false

            backdropImageView.contentMode = .scaleAspectFill
            backdropImageView.clipsToBounds = true
            
            titleLabel.font = UIFont.Heading.medium
            titleLabel.textColor = UIColor.Text.charcoal
            titleLabel.numberOfLines = 0
            titleLabel.lineBreakMode = .byWordWrapping
            titleLabel.setContentHuggingPriority(.required, for: .vertical)
            
            overviewLabel.font = UIFont.Body.small
            overviewLabel.textColor = UIColor.Text.grey
            overviewLabel.numberOfLines = 0
            overviewLabel.lineBreakMode = .byWordWrapping
            
            contentStackView.axis = .vertical
            contentStackView.spacing = 24
            contentStackView.setCustomSpacing(8, after: titleLabel)
            
            setupViewsHierarchy()
            setupConstraints()
        }
        
        private func setupViewsHierarchy() {
            addSubview(scrollView)
            scrollView.addSubview(contentStackView)
        }

        private func setupConstraints() {
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            backdropImageView.translatesAutoresizingMaskIntoConstraints = false
            contentStackView.translatesAutoresizingMaskIntoConstraints = false

            NSLayoutConstraint.activate([

                    scrollView.topAnchor.constraint(equalTo: topAnchor),
                    scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
                    scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
                    scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
                    
                    backdropImageView.heightAnchor.constraint(equalTo: backdropImageView.widthAnchor, multiplier: 11 / 16, constant: 0),
                    
                    contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 24),
                    contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
                    contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
                    contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -24)
                ]
            )
            
            scrollView.layoutMargins = UIEdgeInsets(top: 24, left: 16, bottom: 24, right: 16)
            preservesSuperviewLayoutMargins = false
        }
        
        
        func configure(movieDetails: MovieDetails) {
            backdropImageView.dm_setImage(backdropPath: movieDetails.backdropPath)
            
            titleLabel.text = movieDetails.title
            
            overviewLabel.text = movieDetails.overview
        }
    }
}

Here is the code for SVC code ..

class SmiliarMovieViewController: UIViewController, UICollectionViewDelegate {

    private let viewModel: MoviesDetailsViewModel

    init(viewModel: MoviesDetailsViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
        navigationItem.largeTitleDisplayMode = .never
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    lazy var titleLable: UILabel = {
        let titleLable = UILabel()
        titleLable.translatesAutoresizingMaskIntoConstraints = false
        titleLable.font = UIFont.systemFont(ofSize: 24)
        titleLable.text = "Similar movies"
        return titleLable
    }()

    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let width = 100
        layout.itemSize = CGSize(width: width, height: 200)
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.translatesAutoresizingMaskIntoConstraints = false
        collection.dataSource = self
        collection.delegate = self
        collection.register(SimilierMovieCell.self, forCellWithReuseIdentifier: SimilierMovieCell.identifier)
        collection.backgroundColor = .clear
        return collection
    }()
   
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpUI()
        self.viewModel.updatedState = { [weak self] in
            DispatchQueue.main.async {
                self?.collectionView.reloadData()
            }
        }
        viewModel.fetchSimilarMovie()
    }
    
    private func setUpUI() {
        view.addSubview(titleLable)
        view.addSubview(collectionView)
        
        let safeArea = view.safeAreaLayoutGuide
        titleLable.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
        titleLable.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 10).isActive = true
        titleLable.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
        collectionView.topAnchor.constraint(equalTo: titleLable.bottomAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
       collectionView.heightAnchor.constraint(equalToConstant: view.frame.width/2).isActive = true
    }
}

Here is the cell code ..

class SimilierMovieCell: UICollectionViewCell {
    static let identifier = "CompanyCell"
    let columnSpacing: CGFloat = 16
    let posterSize = CGSize(width: 92, height: 134)
    
    let coverImage = UIImageView()
    let tagView = TagView()
    let titleLabel = UILabel()
    
    let childStackView = UIStackView()
    let containerStackView = UIStackView()

    private func commonInit() {
        layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
        titleLabel.font = UIFont.systemFont(ofSize: 15)
        titleLabel.textColor = UIColor.Text.charcoal
        titleLabel.numberOfLines = 0
        titleLabel.lineBreakMode = .byWordWrapping
        
        coverImage.contentMode = .scaleAspectFit
        coverImage.layer.cornerRadius = 8
        coverImage.layer.masksToBounds = true
        
        childStackView.spacing = 10
        childStackView.alignment = .leading
        childStackView.axis = .vertical
        
        containerStackView.spacing = columnSpacing
        containerStackView.alignment = .top
        containerStackView.translatesAutoresizingMaskIntoConstraints = false

        setupViewsHierarchy()
        setupConstraints()
    }
    
    func setupViewsHierarchy() {
        contentView.addSubview(containerStackView)
        childStackView.dm_addArrangedSubviews(coverImage)
        childStackView.dm_addArrangedSubviews(titleLabel)
        childStackView.dm_addArrangedSubviews(tagView)
        containerStackView.dm_addArrangedSubviews(childStackView)
    }
    //Constraints

    func setupConstraints() {
        
        NSLayoutConstraint.activate([
            containerStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
            containerStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
            containerStackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
            containerStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
            
            coverImage.widthAnchor.constraint(equalToConstant: posterSize.width),
            coverImage.heightAnchor.constraint(equalToConstant: posterSize.height),
            
            titleLabel.topAnchor.constraint(equalTo: coverImage.bottomAnchor),
            tagView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor)
            
        ])
    }
    
    func configure(movie: Movie) {
        commonInit()
       
        titleLabel.text = movie.title
        tagView.configure(.rating(value: movie.voteAverage))
        
        if let path = movie.posterPath {
            coverImage.dm_setImage(posterPath: path)
        } else {
            coverImage.image = nil
        }
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
    }
}

Solution

  • Really, really quick example of using a child view controller...

    We'll start with your "Similar" view controller. A collection view cell and a view controller that creates a collection view:

    class MySimilarCell: UICollectionViewCell {
        let imgView = UIImageView()
        let label = UILabel()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            [imgView, label].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                contentView.addSubview(v)
            }
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                imgView.topAnchor.constraint(equalTo: g.topAnchor),
                imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                label.topAnchor.constraint(equalTo: imgView.bottomAnchor, constant: 4.0),
                label.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            ])
            imgView.backgroundColor = .systemRed
            imgView.tintColor = .white
            label.textAlignment = .center
        }
    }
    
    class MySimilarViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
        
        var cv: UICollectionView!
        let data: [String] = [
            "A", "B", "C", "D", "E", "F", "G", "H"
        ]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            let fl = UICollectionViewFlowLayout()
            fl.scrollDirection = .horizontal
            fl.itemSize = .init(width: 120.0, height: 180.0)
            cv = UICollectionView(frame: .zero, collectionViewLayout: fl)
    
            cv.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(cv)
            
            // so we don't have to type so much
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                cv.topAnchor.constraint(equalTo: g.topAnchor),
                cv.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                cv.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                cv.heightAnchor.constraint(equalToConstant: 180.0),
            ])
            
            cv.dataSource = self
            cv.delegate = self
            cv.register(MySimilarCell.self, forCellWithReuseIdentifier: "c")
            
            // so we can see the collection view framing
            cv.backgroundColor = .cyan
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return data.count
        }
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath) as! MySimilarCell
            if let img = UIImage(systemName: "\(data[indexPath.item].lowercased()).circle") {
                c.imgView.image = img
            }
            c.label.text = data[indexPath.item]
            return c
        }
    }
    

    If you set MySimilarViewController as your Initial View Controller and run the app, it should look like this:

    enter image description here

    Next, we'll setup the "parent" controller, with an image view, a title label and a paragraph label, in a vertical stack view, in a scroll view, with space at the bottom for the "child":

    class MyMainViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
    
            // add an image view, title label, paragraph label
            //  to a vertical stack view
            //      added to a scroll view
            
            let imgView = UIImageView()
            imgView.backgroundColor = .white
            imgView.tintColor = .systemOrange
            if let img = UIImage(systemName: "swift") {
                imgView.image = img
            }
            
            let titleLabel = UILabel()
            titleLabel.backgroundColor = .green
            titleLabel.font = .systemFont(ofSize: 28.0, weight: .bold)
            titleLabel.text = "Spirited Away"
            
            let paraLabel = UILabel()
            paraLabel.backgroundColor = .green
            paraLabel.font = .systemFont(ofSize: 20.0, weight: .regular)
            paraLabel.numberOfLines = 0
            paraLabel.text = "A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family."
    
            let stackView = UIStackView()
            stackView.axis = .vertical
            stackView.spacing = 20.0
    
            stackView.addArrangedSubview(imgView)
            stackView.addArrangedSubview(titleLabel)
            stackView.addArrangedSubview(paraLabel)
    
            let scrollView = UIScrollView()
            
            // so we can see the scroll view's framing
            scrollView.backgroundColor = .systemYellow
            
            stackView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stackView)
    
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(scrollView)
    
            let addHereView = UIView()
            addHereView.backgroundColor = .systemBlue
            
            addHereView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(addHereView)
            
            // so we don't have to type so much
            let g = view.safeAreaLayoutGuide
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                scrollView.bottomAnchor.constraint(equalTo: addHereView.topAnchor, constant: -20.0),
                
                stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 20.0),
                stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20.0),
                stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20.0),
                stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -20.0),
    
                stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -40.0),
                
                // image view needs height
                imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0),
                
                addHereView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                addHereView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                addHereView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
                
                // height of the child content
                addHereView.heightAnchor.constraint(equalToConstant: 180.0),
    
            ])
            
        }
        
    }
    

    Now, if we set MyMainViewController as the Initial View Controller, we should get this:

    enter image description here

    enter image description here

    The image and labels are in the (yellow) scroll view, and we have a blue view where we're going to add the child controller's view.

    So, at the end of viewDidLoad() in MyMainViewController, we'll add this:

        let similarVC = MySimilarViewController()
        addChild(similarVC)
        
        // safely unwrap MySimilarViewController's view
        guard let similarView = similarVC.view else {
            // this should never happen, unless we've really mis-written the detail view controller
            fatalError("Similar VC had no view!!!")
        }
        
        similarView.translatesAutoresizingMaskIntoConstraints = false
        addHereView.addSubview(similarView)
        
        NSLayoutConstraint.activate([
            similarView.topAnchor.constraint(equalTo: addHereView.topAnchor, constant: 0.0),
            similarView.leadingAnchor.constraint(equalTo: addHereView.leadingAnchor, constant: 0.0),
            similarView.trailingAnchor.constraint(equalTo: addHereView.trailingAnchor, constant: 0.0),
            similarView.bottomAnchor.constraint(equalTo: addHereView.bottomAnchor, constant: 0.0),
        ])
        
        similarVC.didMove(toParent: self)
    

    So, we've added an instance of MySimilarViewController as a child view controller, and we've added its view as a subview of the blue view.

    And we get this:

    enter image description here

    enter image description here


    Edit

    If you really, really want to stick with your current approach, couple suggestions...

    1 - Set background colors to your UI elements, so you can see the framing.

    2 - Learn how to use Debug View Hierarchy so you can figure out what's going wrong with your layout.

    After adding (and "faking") the missing information from your post in order to run your code and see the layout, this is what we get:

    enter image description here

    and if we try to scroll the content up, it only goes this far:

    enter image description here

    Let's use Debug View Hierarchy to inspect the layout:

    enter image description here

    You've replaced the controller's built-in view with "parentView" (blue) and you've constrained your scroll view (red) to the full height of the view.

    You've then added "similar" controller's view as a subview, but it's on top of the scroll view.

    So, in parentView -> setupConstraints(), get rid of the scroll view's bottom anchor:

    scrollView.topAnchor.constraint(equalTo: topAnchor),
    scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
    //scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
    scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
    

    Then, in childView(), after adding the current constraints, add this:

    // make sure this controller's view was created as "parentView"
    guard let pView = view as? parentView else {
        fatalError("self.view was not instantiated correctly!")
    }
    // set the bottom constraint of the scroll view in parentView
    //  to the top of addHereView (minus 8-points "spacing")
    pView.scrollView.bottomAnchor.constraint(equalTo: addHereView.topAnchor, constant: -8.0).isActive = true
    
    similarVC.didMove(toParent: self)
    

    Now, when run, we get this result:

    enter image description here

    and we can scroll up all the way to the bottom of the paragraph of text:

    enter image description here

    and our hierarchy now looks like this:

    enter image description here

    The scroll view no longer extends behind the collection view.