Search code examples
iosswiftuiscrollviewuikituiscrollviewdelegate

How to size a UIScrollView to fit an unknown amount of text in a UILabel?


I have added a scrollview subview in one of my views, but am having trouble getting it's height to accurately fit the content that the scrollview is showing, which is text in the UILabel. The height needs to be dynamic (i.e. a factor of the text length), because I am instantiating this view for many different text lengths. Whenever I log label.frame.bounds I get (0,0) back. I have also tried sizeToFits() in a few places without much luck.

My goal is to get the scrollview to end when it reaches the last line of text. Also, I am using only programmatic constraints.

A condensed version of my code is the following:

import UIKit

class ViewController: UIViewController, UIScrollViewDelegate {
    let scrollView = UIScrollView()
    let containerView = UIView()
    let label = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.delegate = self

        // This needs to change
        scrollView.contentSize = CGSize(width: 375, height: 1000)

        scrollView.addSubview(containerView)
        view.addSubview(scrollView)

        label.text = unknownAmountOfText()
        label.backgroundColor = .gray


        containerView.isUserInteractionEnabled = true
        containerView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false

        label.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true

        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.frame = view.bounds
        containerView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
    }
}

Any help is appreciated.

SOLUTION found:

func heightForLabel(text: String, font: UIFont, lineHeight: CGFloat, width: CGFloat) -> CGFloat {
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.lineBreakMode = NSLineBreakMode.byWordWrapping
    label.font = font
    label.text = text
    label.setLineHeight(lineHeight: lineHeight)
    label.sizeToFit()
    return label.frame.height
}

I found this solution online, that gives me what I need to set the appropriate content size for the scrollView height based on the label's height. Ideally, I'd be able to determine this without this function, but for now I'm satisfied.


Solution

  • The key to UIScrollView and its content size is setting your constraints so that the actual content defines the contentSize.

    For a simple example: say you have a UIScrollView with width: 200 and height: 200. Now you put a UIView inside it, that has width: 100 and height: 400. The view should scroll up and down, but not left-right. You can constrain the view to 100x400, and then "pin" the top, bottom, left and right to the sides of the scroll view, and AutoLayout will "auto-magically" set the scrollview's contentSize.

    When you add subviews that can change size - either explicitly (code, user interaction) or implicitly - if the constraints are set correctly those changes will also "auto-magically" adjust the scrollview's contentSize.

    So... here is an example of what you are trying to do:

    import UIKit
    
    class ViewController: UIViewController, UIScrollViewDelegate {
    
        let scrollView = UIScrollView()
        let label = UILabel()
    
        let s1 = "1. This is the first line of text in the label. It has words and punctuation, but no embedded line-breaks, so what you see here is normal UILabel word-wrapping."
        var counter = 1
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // turn off translatesAutoresizingMaskIntoConstraints, because we're going to set them
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            label.translatesAutoresizingMaskIntoConstraints = false
    
            // set background colors, just so we can see the bounding boxes
            self.view.backgroundColor = UIColor(red: 1.0, green: 0.7, blue: 0.3, alpha: 1.0)
            scrollView.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 1.0, alpha: 1.0)
            label.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
    
            // add the label to the scrollView, and the scrollView to the "main" view
            scrollView.addSubview(label)
            self.view.addSubview(scrollView)
    
            // set top, left, right constraints on scrollView to
            // "main" view + 8.0 padding on each side
            scrollView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 8.0).isActive = true
            scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
            scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8.0).isActive = true
    
            // set the height constraint on the scrollView to 0.5 * the main view height
            scrollView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
    
            // set top, left, right AND bottom constraints on label to
            // scrollView + 8.0 padding on each side
            label.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0).isActive = true
            label.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 8.0).isActive = true
            label.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -8.0).isActive = true
            label.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8.0).isActive = true
    
            // set the width of the label to the width of the scrollView (-16 for 8.0 padding on each side)
            label.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -16.0).isActive = true
    
            // configure label: Zero lines + Word Wrapping
            label.numberOfLines = 0
            label.lineBreakMode = NSLineBreakMode.byWordWrapping
            label.font = UIFont.systemFont(ofSize: 17.0)
    
            // set the text of the label
            label.text = s1
    
            // ok, we're done... but let's add a button to change the label text, so we
            // can "see the magic" happening
            let b = UIButton(type: UIButtonType.system)
            b.translatesAutoresizingMaskIntoConstraints = false
            self.view.addSubview(b)
            b.setTitle("Add a Line", for: .normal)
            b.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 24.0).isActive = true
            b.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            b.addTarget(self, action: #selector(self.btnTap(_:)), for: .touchUpInside)
    
        }
    
        func btnTap(_ sender: Any) {
            if let t = label.text {
                counter += 1
                label.text = t + "\n\n\(counter). Another line"
            }
        }
    
    }