Search code examples
iosswiftmobileautolayoutprogrammatically-created

Programmatic auto-layout not calculating height of UIScrollView


I have a a UIViewController which has a header area. At the bottom of that header element is a row of UIButtons. Below these buttons, I've placed a UIScrollView, within which is embedded a UIView called contentContainer. Within that container is a number of UILabels and UIViews styled as dividers.

The entire ViewController, all subviews, and their associated auto-layout constraints are constructed programmatically.

Everything seems to work well, with the exception that the UIScrollView is not vertically scrolling. If I explicitly give the contentContainer subview a size with an auto-layout height constraint, it works.

This leads me to think that height of the contentContainer is not being accurately computed.

Example of how each UI element is built:

let divider1: UIView = {
    let divider = UIView()
    divider.backgroundColor = Colors.white10
    divider.translatesAutoresizingMaskIntoConstraints = false

    return divider
}()

My viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()

    self.view.addSubview(buttonClose)
    self.view.addSubview(labelPlaceTitle)
    self.view.addSubview(buttonShare)
    self.view.addSubview(buttonRoute)
    self.view.addSubview(buttonDelete)
    self.view.addSubview(scrollView)

    scrollView.addSubview(contentContainer)

    contentContainer.addSubview(divider1)
    contentContainer.addSubview(labelAddressLabel)
    contentContainer.addSubview(labelAddressActual)
    contentContainer.addSubview(divider2)
    contentContainer.addSubview(labelDateLabel)
    contentContainer.addSubview(labelDateActual)
    contentContainer.addSubview(divider3)
    contentContainer.addSubview(labelNoteLabel)
    contentContainer.addSubview(labelNoteActual)
    contentContainer.addSubview(buttonAddNote)
    contentContainer.addSubview(divider4)

    setupLayout()
}

My auto-layout constraints:

private func setupLayout() {
    buttonClose.topAnchor.constraint(equalTo: view.topAnchor, constant: 16).isActive = true
    buttonClose.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16).isActive = true
    buttonClose.widthAnchor.constraint(equalToConstant: 28).isActive = true
    buttonClose.heightAnchor.constraint(equalToConstant: 28).isActive = true

    labelPlaceTitle.topAnchor.constraint(equalTo: view.topAnchor, constant: 48).isActive = true
    labelPlaceTitle.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 24).isActive = true
    labelPlaceTitle.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -24).isActive = true

    buttonShare.topAnchor.constraint(equalTo: labelPlaceTitle.bottomAnchor, constant: 16).isActive = true
    buttonShare.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 24).isActive = true
    buttonShare.widthAnchor.constraint(equalToConstant: 48).isActive = true
    buttonShare.heightAnchor.constraint(equalToConstant: 48).isActive = true

    buttonRoute.topAnchor.constraint(equalTo: labelPlaceTitle.bottomAnchor, constant: 16).isActive = true
    buttonRoute.leftAnchor.constraint(equalTo: buttonShare.rightAnchor, constant: 8).isActive = true
    buttonRoute.widthAnchor.constraint(equalToConstant: 48).isActive = true
    buttonRoute.heightAnchor.constraint(equalToConstant: 48).isActive = true

    buttonDelete.topAnchor.constraint(equalTo: labelPlaceTitle.bottomAnchor, constant: 16).isActive = true
    buttonDelete.leftAnchor.constraint(equalTo: buttonRoute.rightAnchor, constant: 8).isActive = true
    buttonDelete.widthAnchor.constraint(equalToConstant: 48).isActive = true
    buttonDelete.heightAnchor.constraint(equalToConstant: 48).isActive = true

    scrollView.topAnchor.constraint(equalTo: buttonRoute.bottomAnchor, constant: 32).isActive = true
    scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
    scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
    scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true

    contentContainer.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true
    contentContainer.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: 0).isActive = true
    contentContainer.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 0).isActive = true
    contentContainer.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0).isActive = true
    contentContainer.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor).isActive = true
    contentContainer.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true
    contentContainer.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true

    divider1.topAnchor.constraint(equalTo: contentContainer.topAnchor, constant: 24).isActive = true
    divider1.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    divider1.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true
    divider1.heightAnchor.constraint(equalToConstant: 1).isActive = true

    labelAddressLabel.topAnchor.constraint(equalTo: divider1.bottomAnchor, constant: 12).isActive = true
    labelAddressLabel.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelAddressLabel.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    labelAddressActual.topAnchor.constraint(equalTo: labelAddressLabel.bottomAnchor, constant: 4).isActive = true
    labelAddressActual.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelAddressActual.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    divider2.topAnchor.constraint(equalTo: labelAddressActual.bottomAnchor, constant: 12).isActive = true
    divider2.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    divider2.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true
    divider2.heightAnchor.constraint(equalToConstant: 1).isActive = true

    labelDateLabel.topAnchor.constraint(equalTo: divider2.bottomAnchor, constant: 12).isActive = true
    labelDateLabel.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelDateLabel.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    labelDateActual.topAnchor.constraint(equalTo: labelDateLabel.bottomAnchor, constant: 4).isActive = true
    labelDateActual.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelDateActual.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    divider3.topAnchor.constraint(equalTo: labelDateActual.bottomAnchor, constant: 12).isActive = true
    divider3.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    divider3.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true
    divider3.heightAnchor.constraint(equalToConstant: 1).isActive = true

    labelNoteLabel.topAnchor.constraint(equalTo: divider3.bottomAnchor, constant: 12).isActive = true
    labelNoteLabel.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelNoteLabel.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    labelNoteActual.topAnchor.constraint(equalTo: labelNoteLabel.bottomAnchor, constant: 4).isActive = true
    labelNoteActual.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    labelNoteActual.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true

    buttonAddNote.topAnchor.constraint(equalTo: labelNoteActual.bottomAnchor, constant: 12).isActive = true
    buttonAddNote.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    buttonAddNote.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true
    buttonAddNote.heightAnchor.constraint(equalToConstant: 32).isActive = true

    divider4.topAnchor.constraint(equalTo: buttonAddNote.bottomAnchor, constant: 12).isActive = true
    divider4.leftAnchor.constraint(equalTo: contentContainer.leftAnchor, constant: 24).isActive = true
    divider4.rightAnchor.constraint(equalTo: contentContainer.rightAnchor, constant: -24).isActive = true
    divider4.heightAnchor.constraint(equalToConstant: 1).isActive = true
}

}

And a shot of the UI, with the light grey portion of the sheet representing the contentContainer:

Screenshot of UI

Thanks for your help.


Solution

  • In order to compute the height of your contentContainer, Auto Layout needs an unbroken chain of subviews from the top of contentContainer to the bottom. In your case, you seem to have that chain except that your bottommost view is not connected to the bottom of the contentContainer.

    Add a constraint connecting the bottom of divider4 to the bottom of contentContainer. This will allow Auto Layout to compute the size of contentContainer.

    Also, you shouldn't set these constraints:

    contentContainer.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor).isActive = true
    contentContainer.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true