Search code examples
iosswiftuiscrollview

How to make a programatic scrollviews container height the exact size of its content in Swift?


My problem is as follows:

The height of contentView is taller than its subviews, so although the subviews of contentView don't take up the entire screen vertically, you can still scroll past the content.

How can I set the height of contentView to match the height of its subviews, so that way if the screen is large enough vertically to fit the content, you can't scroll, but if the screen is smaller or a keyboard signal is sent, then you can scroll vertically?

I understand the problem is with the height constraint in the contentView.anchor, but I don't know how to fix it and I haven't been able to find any answers.

let scrollView = UIScrollView()
let contentView = UIView()

override func viewDidLoad() {
    super.viewDidLoad()
    
    configureScrollView()
    configureUI()
    
}

func configureScrollView() {
            
    view.addSubview(scrollView)
    scrollView.anchor(top: view.safeAreaLayoutGuide.topAnchor,
                      left: view.leftAnchor,
                      bottom: view.safeAreaLayoutGuide.bottomAnchor,
                      right: view.rightAnchor)
    
    scrollView.addSubview(contentView)
    contentView.anchor(top: scrollView.topAnchor,
                       left: scrollView.leftAnchor,
                       bottom: scrollView.bottomAnchor,
                       right: scrollView.rightAnchor,
                       width: view.frame.size.width,
                       height: view.frame.size.height)
}

func configureUI() {
    
    contentView.addSubview(previewTitleLabel)
    previewTitleLabel.anchor(top: contentView.topAnchor,
                             left: contentView.leftAnchor,
                             right: contentView.rightAnchor,
                             paddingTop: 20,
                             paddingLeft: 20,
                             paddingRight: 20,
                             height: 20)

    contentView.addSubview(previewView)
    previewView.anchor(top: previewTitleLabel.bottomAnchor,
                       left: contentView.leftAnchor,
                       right: contentView.rightAnchor,
                       paddingTop: 20,
                       paddingLeft: 20,
                       paddingRight: 20) // Has dynamic height

    contentView.addSubview(detailsTitleLabel)
    detailsTitleLabel.anchor(top: previewView.bottomAnchor,
                             left: contentView.leftAnchor,
                             right: contentView.rightAnchor,
                             paddingTop: 20,
                             paddingLeft: 20,
                             paddingRight: 20,
                             height: 20)

    contentView.addSubview(descriptionView)
    descriptionView.anchor(top: detailsTitleLabel.bottomAnchor,
                           left: contentView.leftAnchor,
                           right: contentView.rightAnchor,
                           paddingTop: 20,
                           paddingLeft: 20,
                           paddingRight: 20,
                           height: 278)
}

Solution

  • Couple tips:

    1. Go through a half-dozen or so scroll view tutorials, so you understand how they work.
    2. Go through a half-dozen or so auto-layout tutorials, so you understand how constraints work.
    3. Use standard constraint syntax instead of your .anchor() "helper" until you fully understand how constraints work. This also lets you "group" your constraints logically, as you'll see in this example code.
    4. During layout development, give your UI elements contrasting background colors to make it easy to see frames.

    So, example code:

    class ExampleScrollViewController: UIViewController {
    
        let scrollView = UIScrollView()
        let contentView = UIView()
        
        let previewTitleLabel = UILabel()
        let previewView = UIView()
        let detailsTitleLabel = UILabel()
        let descriptionView = UIView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            configureScrollView()
            configureUI()
            
            // use some background colors so we can easily see frames
            scrollView.backgroundColor = .red
            contentView.backgroundColor = .blue
            previewTitleLabel.backgroundColor = .cyan
            previewView.backgroundColor = .green
            detailsTitleLabel.backgroundColor = .cyan
            descriptionView.backgroundColor = .green
    
            previewTitleLabel.text = "Preview Title Label"
            detailsTitleLabel.text = "Details Title Label"
        }
        
        func configureScrollView() {
    
            contentView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.translatesAutoresizingMaskIntoConstraints = false
    
            scrollView.addSubview(contentView)
            view.addSubview(scrollView)
            
            // respect safe area
            let g = view.safeAreaLayoutGuide
            
            // reference to scrollView's contentLayoutGuide
            let svContentLG = scrollView.contentLayoutGuide
            
            // reference to scrollView's frameLayoutGuide
            let svFrameLG = scrollView.frameLayoutGuide
            
    
            NSLayoutConstraint.activate([
                
                // constrain scroll view to full safe area
                scrollView.topAnchor.constraint(equalTo: g.topAnchor),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
    
                // constrain content view to all 4 sides of scroll view's content layout guide
                contentView.topAnchor.constraint(equalTo: svContentLG.topAnchor),
                contentView.leadingAnchor.constraint(equalTo: svContentLG.leadingAnchor),
                contentView.trailingAnchor.constraint(equalTo: svContentLG.trailingAnchor),
                contentView.bottomAnchor.constraint(equalTo: svContentLG.bottomAnchor),
                
                // we want vertical scrolling,
                //  so constrain width of content view to scroll view's frame
                contentView.widthAnchor.constraint(equalTo: svFrameLG.widthAnchor),
    
            ])
    
        }
        
        func configureUI() {
            
            [previewTitleLabel, previewView, detailsTitleLabel, descriptionView].forEach {
                $0.translatesAutoresizingMaskIntoConstraints = false
                contentView.addSubview($0)
            }
            
            NSLayoutConstraint.activate([
    
                // horizontal constraints
                
                // all 4 subviews will be constrained
                //  Leading and Trailing to the contentView
                //  with 20-pts "padding" on left and right
                previewTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
                previewTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
                previewView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
                previewView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
                detailsTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
                detailsTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
                descriptionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20.0),
                descriptionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20.0),
    
                // vertical spacing constraints
                
                // previewTitleLabel Top 20-pts from contentView Top
                previewTitleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
    
                // previewView Top 20-pts from previewTitleLabel Bottom
                previewView.topAnchor.constraint(equalTo: previewTitleLabel.bottomAnchor, constant: 20.0),
    
                // detailsTitleLabel Top 20-pts from previewView Bottom
                detailsTitleLabel.topAnchor.constraint(equalTo: previewView.bottomAnchor, constant: 20.0),
    
                // descriptionView Top 20-pts from detailsTitleLabel Bottom
                descriptionView.topAnchor.constraint(equalTo: detailsTitleLabel.bottomAnchor, constant: 20.0),
    
                // complete the vertical spacing constraints by
                //  constraining descriptionView Bottom 20-pts from contentView Bottom
                descriptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
    
                // vertical height constraints
                
                // previewTitleLabel Height: 20
                previewTitleLabel.heightAnchor.constraint(equalToConstant: 20.0),
                
                // previewView Height: 20
                previewView.heightAnchor.constraint(equalToConstant: 20.0),
                
                // detailsTitleLabel Height: 20
                detailsTitleLabel.heightAnchor.constraint(equalToConstant: 20.0),
                
                // descriptionView Height: 270
                descriptionView.heightAnchor.constraint(equalToConstant: 270.0),
    
            ])
    
        }
        
    }
    

    Result:

    enter image description here

    If I change the Height of previewView also to 270:

    previewView.heightAnchor.constraint(equalToConstant: 270.0),
    

    we get this result:

    enter image description here

    and we can scroll up to see the bottom of the content:

    enter image description here