Search code examples
iosuiscrollviewautolayout

UIScrollView setup with AutoLayout Programmatically


I know this question has been asked thousand times but I am still not able to get it right.

Here are the constraints I used

  1. Added ScrollView to View. Set ScrollView top/left/right/bottom anchors to view.
  2. Added ContainerView to ScrollView. Set ContainerView top/left/right/bottom anchors to ScrollView.contentLayoutGuide. Added Width and height constraint of ContainerView to ScrollView.frameLayoutGuide and Added priority to height as low.
  3. Added 1 label to ContainerView with top(wrt ContainerView)/left(wrt ContainerView)/right(wrt ContainerView)
  4. Added ContentView to ContainerView by setting top(wrt Label)/left(wrt ContainerView)/right(wrt ContainerView)/bottom(wrt ContainerView) anchors with some constant.
  5. Added UIImageView, UILabel and UITableView to ContentView.
  6. Set ImageView top/left/right/height anchors wrt to ContentView.
  7. Set label top(wrt ImageView)/left(wrt ContentView)/right(wrt ContentView) constant
  8. Set TableView top(wrt Label)/left(wrt ContentView)/right(wrt ContentView)/height(constant) anchors.

Here is the code

    self.view.addSubview(self.scrollView)
    self.scrollView.addSubview(self.containerView)
        
        self.scrollView.translatesAutoresizingMaskIntoConstraints = false
        self.containerView.translatesAutoresizingMaskIntoConstraints = false
        self.scrollView.showsVerticalScrollIndicator = false
        
        self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        self.scrollView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
        self.scrollView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
        self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true

        let layoutGuide = scrollView.contentLayoutGuide
        
        self.containerView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
        self.containerView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
        self.containerView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
        self.containerView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
        self.containerView.widthAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.widthAnchor).isActive = true
        self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).isActive = true
        self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).priority = .defaultLow
  
    self.containerView.addSubview(self.titleLabel)
    self.containerView.addSubview(self.instructionStackView)
    self.containerView.addSubview(self.contentView)
    
    
    self.contentView.addSubview(self.questionImageView)
    self.contentView.addSubview(self.questionTitleLabel)
    self.contentView.addSubview(self.quizOptionsTableView)
    
    self.titleLabel.topAnchor.constraint(equalTo: self.containerView.topAnchor, constant: 16).isActive = true
    self.titleLabel.leftAnchor.constraint(equalTo: self.containerView.leftAnchor).isActive = true
    self.titleLabel.rightAnchor.constraint(equalTo: self.containerView.rightAnchor).isActive = true

    self.instructionStackView.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 24).isActive = true
    self.instructionStackView.leftAnchor.constraint(equalTo: self.containerView.leftAnchor, constant: 50).isActive = true
    self.instructionStackView.rightAnchor.constraint(equalTo: self.containerView.rightAnchor, constant: -50).isActive = true

    let verticalStackView = UIStackView(frame: .zero)
    verticalStackView.axis = .vertical
    verticalStackView.addArrangedSubview(self.questionNumberLabel)
    verticalStackView.addArrangedSubview(self.marksLabel)
    self.questionNumberLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)

    let emptyView = UIView(frame: .zero)
    emptyView.widthAnchor.constraint(equalToConstant: 40).isActive = true

    let horizantalStackView = UIStackView(frame: .zero)
    horizantalStackView.axis = .horizontal
    horizantalStackView.addArrangedSubview(emptyView)
    horizantalStackView.addArrangedSubview(self.clockImageView)
    horizantalStackView.addArrangedSubview(self.timerLabel)
    horizantalStackView.spacing = 8


    self.clockImageView.widthAnchor.constraint(equalToConstant: 46).isActive = true
    self.clockImageView.heightAnchor.constraint(equalToConstant: 46).isActive = true

    self.instructionStackView.addArrangedSubview(verticalStackView)
    self.instructionStackView.addArrangedSubview(horizantalStackView)
    
    self.contentView.topAnchor.constraint(equalTo: self.instructionStackView.bottomAnchor,
                                          constant: 16).isActive = true
    self.contentView.leftAnchor.constraint(equalTo: self.containerView.leftAnchor,
                                           constant: 50).isActive = true
    self.contentView.rightAnchor.constraint(equalTo: self.containerView.rightAnchor,
                                          constant: -50).isActive = true
    self.contentView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor,
                                             constant: -16).isActive = true
    

    self.questionImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor,
                                          constant: 8).isActive = true
    self.questionImageView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor,
                                           constant: 8).isActive = true
    self.questionImageView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor,
                                          constant: -8).isActive = true
    self.questionImageView.heightAnchor.constraint(equalToConstant: 170).isActive = true
    
    self.questionTitleLabel.topAnchor.constraint(equalTo: self.self.questionImageView.bottomAnchor,
                                          constant: 8).isActive = true
    self.questionTitleLabel.leftAnchor.constraint(equalTo: self.contentView.leftAnchor,
                                           constant: 8).isActive = true
    self.questionTitleLabel.rightAnchor.constraint(equalTo: self.contentView.rightAnchor,
                                          constant: -8).isActive = true
    self.questionTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
    
    self.quizOptionsTableView.topAnchor.constraint(equalTo: self.questionTitleLabel.bottomAnchor,
                                                   constant: 8).isActive = true
    self.quizOptionsTableView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor,
                                           constant: 8).isActive = true
    self.quizOptionsTableView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor,
                                          constant: -8).isActive = true
    self.quizOptionsTableView.heightAnchor.constraint(equalToConstant: 320).isActive = true

View is scrollable with no warnings. But does not scroll to bottom. Where I am wrong?


Solution

  • You don't need either of these lines - remove them:

    self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).isActive = true
    self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).priority = .defaultLow
    

    You have no constraint controlling the height of contentView ... you need to add:

    // quizOptionsTableView bottom to contentView bottom with 8-points "padding
    self.quizOptionsTableView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true
    

    Couple of suggestions...

    • Respect the Safe Area
    • Stick with .leadingAnchor and .trailingAnchor (right now you're mixing in left/right).
    • Group actions together ... that is, do your subview adding in one place, your constraints all together ... your UI element properties all together.
    • Give your UI elements contrasting background colors to make it easy to see the frames.

    Very STRONGLY Recommend: use comments!!!!

    Take a look at the way I've edited your code:

    class SampleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        
        let scrollView = UIScrollView()
        let titleLabel = UILabel()
        let containerView = UIView()
        let contentView = UIView()
    
        let questionImageView = UIImageView()
        let questionTitleLabel = UILabel()
        let questionNumberLabel = UILabel()
        let marksLabel = UILabel()
        let quizOptionsTableView = UITableView()
        
        let instructionStackView = UIStackView()
    
        let clockImageView = UIImageView()
        let timerLabel = UILabel()
    
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            [scrollView, containerView, contentView, titleLabel, instructionStackView,
             questionImageView, questionTitleLabel, questionNumberLabel,
             marksLabel, quizOptionsTableView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
            }
    
            //self.scrollView.showsVerticalScrollIndicator = false
    
            self.view.addSubview(self.scrollView)
            self.scrollView.addSubview(self.containerView)
    
            self.containerView.addSubview(self.titleLabel)
            self.containerView.addSubview(self.instructionStackView)
            self.containerView.addSubview(self.contentView)
            
            self.contentView.addSubview(self.questionImageView)
            self.contentView.addSubview(self.questionTitleLabel)
            self.contentView.addSubview(self.quizOptionsTableView)
            
            self.questionTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
    
            let verticalStackView = UIStackView(frame: .zero)
            verticalStackView.axis = .vertical
            verticalStackView.addArrangedSubview(self.questionNumberLabel)
            verticalStackView.addArrangedSubview(self.marksLabel)
            self.questionNumberLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
            
            let emptyView = UIView(frame: .zero)
            emptyView.widthAnchor.constraint(equalToConstant: 40).isActive = true
            
            let horizantalStackView = UIStackView(frame: .zero)
            horizantalStackView.axis = .horizontal
            horizantalStackView.addArrangedSubview(emptyView)
            horizantalStackView.addArrangedSubview(self.clockImageView)
            horizantalStackView.addArrangedSubview(self.timerLabel)
            horizantalStackView.spacing = 8
    
            self.instructionStackView.addArrangedSubview(verticalStackView)
            self.instructionStackView.addArrangedSubview(horizantalStackView)
    
            let safeGuide = view.safeAreaLayoutGuide
            let layoutGuide = scrollView.contentLayoutGuide
            
            NSLayoutConstraint.activate([
    
            // all 4 sides of scrollView to view
            self.scrollView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
            self.scrollView.leftAnchor.constraint(equalTo: safeGuide.leftAnchor),
            self.scrollView.rightAnchor.constraint(equalTo: safeGuide.rightAnchor),
            self.scrollView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
            
            // all 4 sides of containerView to scrollView's Content Layout Guide
            self.containerView.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
            self.containerView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
            self.containerView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
            self.containerView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor),
            
            // containerView Width to scrollView's Frame Layout Guide
            self.containerView.widthAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.widthAnchor),
            
            // NO height constraint for containerView
            
            // titleLabel to top of containerView + 16-points "padding"
            self.titleLabel.topAnchor.constraint(equalTo: self.containerView.topAnchor, constant: 16),
            // titleLabel to leading/trailing of containerView
            self.titleLabel.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor),
            self.titleLabel.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor),
            
            // instructionStackView top to titleLabel bottom with 24-points "padding"
            self.instructionStackView.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 24),
            // instructionStackView to leading/trailing of containerView with 50-points "padding"
            self.instructionStackView.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor, constant: 50),
            self.instructionStackView.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor, constant: -50),
            
            // clockImageView width and height
            self.clockImageView.widthAnchor.constraint(equalToConstant: 46),
            self.clockImageView.heightAnchor.constraint(equalToConstant: 46),
            
            // contentView top to instructionStackView bottom + 16-points "padding"
            self.contentView.topAnchor.constraint(equalTo: self.instructionStackView.bottomAnchor, constant: 16),
            // contentView to leading/trailing of containerView with 50-points "padding"
            self.contentView.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor, constant: 50),
            self.contentView.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor, constant: -50),
            // contentView bottom to containerView bottom with 16-points "padding"
            self.contentView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor, constant: -16),
            
            // questionImageView top/leading/trailing to contentView with 8-points "padding"
            self.questionImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8),
            self.questionImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
            self.questionImageView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),
            // questionImageView height constant
            self.questionImageView.heightAnchor.constraint(equalToConstant: 170),
            
            // questionTitleLabel top to questionImageView bottom + 8-points "padding"
            self.questionTitleLabel.topAnchor.constraint(equalTo: self.self.questionImageView.bottomAnchor, constant: 8),
            // questionTitleLabel leading/trailing to contentView with 8-points "padding"
            self.questionTitleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
            self.questionTitleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),
            
            // quizOptionsTableView top to questionTitleLabel + 8-points "padding
            self.quizOptionsTableView.topAnchor.constraint(equalTo: self.questionTitleLabel.bottomAnchor, constant: 8),
            // quizOptionsTableView leading/trailing to contentView with 8-points "padding"
            self.quizOptionsTableView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
            self.quizOptionsTableView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),
            // quizOptionsTableView height constant
            self.quizOptionsTableView.heightAnchor.constraint(equalToConstant: 320),
    
            // quizOptionsTableView bottom to contentView bottom with 8-points "padding
            self.quizOptionsTableView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8),
            ])
            
            // UI element properties
            titleLabel.numberOfLines = 0
            titleLabel.textAlignment = .center
            titleLabel.text = "This is the text for the Title Label which should be able to wrap onto multiple lines."
            
            questionTitleLabel.numberOfLines = 0
            questionTitleLabel.text = "This is the text for the Question Title Label which should be able to wrap onto multiple lines just like the Title Label."
            
            questionNumberLabel.text = "1"
            marksLabel.text = "Marks?"
            
            if let img = UIImage(systemName: "clock.fill") {
                clockImageView.image = img
            }
            if let img = UIImage(systemName: "photo.tv") {
                questionImageView.image = img
            }
            
            quizOptionsTableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
            quizOptionsTableView.dataSource = self
            quizOptionsTableView.delegate = self
            
            // let's give our UI elements some constrasting colors so we can see their frames
            view.backgroundColor = .lightGray
            scrollView.backgroundColor = .red
            containerView.backgroundColor = .systemGreen
            contentView.backgroundColor = .systemBlue
            titleLabel.backgroundColor = .yellow
            questionTitleLabel.backgroundColor = .cyan
            marksLabel.backgroundColor = .green
            clockImageView.backgroundColor = .systemYellow
            questionImageView.backgroundColor = .systemYellow
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 20
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
            c.textLabel?.text = "\(indexPath)"
            return c
        }
    
    }