Home > Mobile >  UIScrollView setup with AutoLayout Programmatically
UIScrollView setup with AutoLayout Programmatically

Time:06-19

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?

CodePudding user response:

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
    }

}
  • Related