Search code examples
iosobjective-cuiscrollviewios-autolayout

Resizing a multi line UILabel within a UIScrollView breaks scrolling. Why?


I use the following setup:

  • The ViewControllers view holds a UIScrollView with Top, Leading, Trailing and Bottom constraints to match the VCs size
  • The ScrollView contains two subviews:
    • A UIView to define the content size of the scroll view. It has the same height as the ScrollView but twice its width. Thus only horizontal scrolling is possible.
    • A UILabel with some long text with a Height and Width constraint to set a fixed size and a Top and Leading constraint to the ScrollView to set a fixed position.
  • The width of the Label is changed when the ScrollView scrolls.

Problem: If the Label is set to use more than one line AND the ScrollViews contenOffset property is set manually the ScrollView stops scrolling.

ViewController View
+---------------------+
|+-------------------+| 
||ScrollView         ||
||+------------------||--------------------+
|||UIView to define  || content size       |
|||                  ||                    |
|||                  ||                    |
|||  [MultiLine]     ||                    |
|||  [  Label  ]     ||                    |
|||                  ||                    |
||+------------------||--------------------+
|+-------------------+|
+---------------------+


- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    // Setting the ContentOffset will stop scrolling 
    //[self.scrollView setContentOffset:CGPointMake(0, 0)];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // Resize Label when scrolling
    self.labelWidthConstraint.constant = MAX (50, 50 + self.scrollView.contentOffset.x);
}

Resizing the label using this code works fine if

  • the label is set use one line. In this case setting the setting the content offset does NOT do any harm. OR
  • the content offset is not changes (not even set to (0, 0)). In this case setting the label to multi line does NOT do any harm

Setting the content offset AND using multi line at the same time DOES NOT work. The scroll cannot be scrolled any more.

Why is this? Any idea what might cause this and how to solve it?


Solution

  • The issue appears to be that when the label constraints are changed it triggers viewDidLayoutSubviews which then sets the UIScrollView to not scroll since set contentOffset is then called over and over. You could overcome this if you are only wanting to set the UIScrollView to CGPoint.zero on the initial layout by using a bool as a flag. Apparently since UILabel needs a redraw on size changes it triggers viewDidLayoutSubviews. Here is an example in Swift.

    import UIKit
    
    class ViewController: UIViewController {
    
        lazy var scrollView : UIScrollView = {
            let sv = UIScrollView(frame: self.view.bounds)
            sv.translatesAutoresizingMaskIntoConstraints = false
            sv.delegate = self
            return sv
        }()
    
        lazy var contentView : UIView = {
            let v = UIView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width * 4, height: self.view.bounds.height))
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
    
        lazy var label : UILabel = {
            let lbl = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 50))
            lbl.numberOfLines = 0
            lbl.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"
            lbl.minimumScaleFactor = 0.5
            lbl.adjustsFontSizeToFitWidth = true
            lbl.font = UIFont.systemFont(ofSize: 22)
            lbl.translatesAutoresizingMaskIntoConstraints = false
            return lbl
        }()
    
        var widthConstraint : NSLayoutConstraint?
        var heightConstraint : NSLayoutConstraint?
        var startingHeight : CGFloat = 0
        var startingWidth : CGFloat = 0
        override func viewDidLoad() {
            super.viewDidLoad()
            //first scrollview
            self.view.addSubview(scrollView)
            pinToAllSides(target: scrollView)
    
            //now content view
            self.scrollView.addSubview(contentView)
            contentView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 2).isActive = true
            contentView.heightAnchor.constraint(equalTo: self.scrollView.heightAnchor, multiplier: 1).isActive = true
            contentView.backgroundColor = .green
            pinToAllSides(target: contentView)
            scrollView.layoutIfNeeded()
    
            //now the label
            self.scrollView.addSubview(label)
            label.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor, constant: 20).isActive = true
            label.topAnchor.constraint(equalTo: self.scrollView.topAnchor, constant: 60).isActive = true
            label.backgroundColor = .red
            widthConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: self.view.bounds.width/2)
    
            heightConstraint = NSLayoutConstraint(item: label, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 300)
            if let wc = widthConstraint,
                let hc = heightConstraint{
                startingHeight = hc.constant
                startingWidth = wc.constant
                label.addConstraint(wc)
                label.addConstraint(hc)
            }
    
        }
    
        func pinToAllSides(target:UIView){
            guard let superview = target.superview else{
                return
            }
            target.leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true
            target.trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true
            target.topAnchor.constraint(equalTo: superview.topAnchor).isActive = true
            target.bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true
        }
    
        var hasHappenedOnce : Bool = false
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            if hasHappenedOnce == false{
                hasHappenedOnce = true
                self.scrollView.contentOffset = .zero
            }
        }
    }
    
    extension ViewController : UIScrollViewDelegate{
    
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            //hopefully it is laggy due to simulator but for the label i would ditch constraints myself
            self.widthConstraint?.constant = max(startingWidth, self.scrollView.contentOffset.x * 1.1 + startingWidth)
    
            let height = startingHeight - self.scrollView.contentOffset.x
            self.heightConstraint?.constant = height
            label.updateConstraints()
        }
    }