Search code examples
iosswiftuiscrollviewautolayoutconstraints

Right anchor of UIScrollView does not apply


I have a UIViewController? SingleEventController displaying Events. Because it has dynamic information to show, I chose to create a new class UIScrollView EventScrollView. I tried to realize that based on this very well explained Answer. I tried to apply the Pure Auto Layout Approach: Create another UIView contentView in it, which contains all the information and is anchored to all four anchors of the scrollView.

The contentSize of the scrollView is determined in the viewDidLayoutSubviews function in the SingleEventController.

My struggle is, that the rightAnchor seems to be faulty. All the information-Elements go over the border of the view, the word-wrapping for the larger UILabels does not work, and every Item layed out at the right anchor is gone. (For example the usernames in the UITableView nopePeopleTV which is a subview of the contentView.)

This looks like this: Problem-Image


Code in the SingleEventController:

view.addSubview(scrollView)
scrollView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: buttonViewDividerView.topAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)

override func viewDidLayoutSubviews() {
    let heightOfAllObjects = scrollView.calculateHeightOfAllObjects()
    scrollView.contentSize = CGSize(width: self.view.frame.width, height: heightOfAllObjects + scrollView.heightOfAllPaddings)
}

Code in EventScrollView:

addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(locationLabel)
...
contentView.addSubview(nopePeopleTV)

contentView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
titleLabel.anchor(top: contentView.topAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 10, paddingLeft: padding, paddingBottom: 0, paddingRight: padding, width: 0, height: 0)
locationLabel.anchor(top: titleLabel.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: padding, paddingBottom: 0, paddingRight: 0, width: 200, height: 0)
nopePeopleTV.anchor(top: maybePeopleTV.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)

layoutIfNeeded()
contentView.layoutIfNeeded()

What am I doing wrong?


My anchor function looks like this:

func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?,  paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {

    translatesAutoresizingMaskIntoConstraints = false

    if let top = top {
        topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
    }

    if let left = left {
        leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
    }

    if let bottom = bottom {
        bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
    }

    if let right = right {
        rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
    }

    if width != 0 {
        widthAnchor.constraint(equalToConstant: width).isActive = true
    }

    if height != 0 {
        heightAnchor.constraint(equalToConstant: height).isActive = true
    }
}

Solution

  • The problem is that you are letting the content view width be dictated from the inside out by the width of the subviews. You need to give the content view a width constraint matching the width of the scroll view itself.

    I'll demonstrate the difference with an artificial but complete code-only example:

    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            let sv = UIScrollView()
            sv.translatesAutoresizingMaskIntoConstraints = false
            self.view.addSubview(sv)
            NSLayoutConstraint.activate([
                sv.topAnchor.constraint(equalTo: self.view.topAnchor),
                sv.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                sv.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                sv.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            ])
            let cv = UIView() // content view
            cv.translatesAutoresizingMaskIntoConstraints = false
            sv.addSubview(cv)
            NSLayoutConstraint.activate([
                sv.topAnchor.constraint(equalTo: cv.topAnchor),
                sv.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
                sv.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
                sv.bottomAnchor.constraint(equalTo: cv.bottomAnchor),
            ])
            let lab = UILabel()
            lab.translatesAutoresizingMaskIntoConstraints = false
            lab.numberOfLines = 0
            lab.text = Array(repeating: "xxx", count: 100).joined(separator: " ")
            cv.addSubview(lab)
            NSLayoutConstraint.activate([
                lab.topAnchor.constraint(equalTo: cv.topAnchor, constant:30),
                lab.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
                lab.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
            ])
        }
    }
    

    Here's the result:

    enter image description here

    We have a very long label, but it doesn't wrap: it keeps extending to the right, off the screen. That's because the content view itself has its right edge off the screen. That's because we have done nothing to prevent this from happening. The long label is itself making the content view as wide as the label's entire text.

    Now I add one final line of code to viewDidLoad:

        cv.widthAnchor.constraint(
            equalTo:sv.frameLayoutGuide.widthAnchor).isActive = true
    

    The result is this:

    enter image description here

    The content view is now the width of the scroll view, so the label wraps within the scroll view.

    We do not, however, do the same thing to the height of the content view; we want it to grow vertically in accordance with its subviews. And that way, we will be able to scroll vertically (which is what you want) but not horizontally (which is also what you want).