UIStackView inside UIScrollView, grows until maximum height then scrolls
Asked Answered
D

2

7

I'm looking to build a view which will grow as subviews are added to it. At first the height will be zero, then should grow as I add subviews until it reaches a maximum size, and then I want it to become scrollable.

If I build this using a UIView inside a UIScrollView and set up the constraints from top to bottom manually, it works as expected. However, I feel like this should be possible using a UIStackView, but when I try either the scroll view doesn't grow, or the contentSize of the scrollView gets stuck at the maximum height, and won't scroll. I've been playing with this for a while and think it might actually be a bug with UIStackView and Autolayout, but hopefully I just missed something. Wrapping the UIStackView in a container UIView doesn't help.

Here's a complete view controller which shows the problem (tapping anywhere adds labels to the scroll view)

import UIKit

class DumbScrollStack: UIViewController {

    let stack = UIStackView()
    let scrollView = UIScrollView()

    override func viewDidLoad() {
        super.viewDidLoad()

        //Setup
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(addLabel)))
        stack.axis = .vertical
        view.backgroundColor = .blue
        scrollView.backgroundColor = .red

        //Add scroll view and setup position
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        scrollView.topAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true

        //Set maximum height
        scrollView.heightAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true

        //add stack
        scrollView.addSubview(stack)
        stack.translatesAutoresizingMaskIntoConstraints = false

        //setup scrollView content size constraints
        stack.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        stack.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
        stack.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
        stack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
        stack.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true

        // Keep short if stack height is lower than scroll height
        // With this constraint the scrollView grows as expected, but doesn't scroll when it goes over 100px
        // Without this constraint the scrollView is always 100px tall but it does scroll
        let constraint = scrollView.heightAnchor.constraint(equalTo: stack.heightAnchor)
        constraint.priority = UILayoutPriority(rawValue: 999)
        constraint.isActive = true

        addLabel()
    }

    @objc func addLabel() {
        let label = UILabel()
        label.backgroundColor = .gray
        label.text = "Hello"
        stack.addArrangedSubview(label)
    }
}
Dillie answered 22/1, 2019 at 17:39 Comment(2)
Is there a reason you are trying to resize your scrollView?Predisposition
I found this code example incredibly useful to make my scrollView dynamically size appropriately. But it did take me awhile to figure out how you were doing it. I think the key point from it that others should note is that there are TWO heightAnchor constraints. The first one is a range (via lessThanOrEqualToConstant) The second one is equal to its contents size but has a lower priority. Ideally, AutoLayout can make both of them valid, but if it can't, it knows to ignore the second one, which makes the actual contents bigger than the scrollView and thereby enables scrolling.Labour
P
9

Add this

label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .vertical)

or comment this block

let constraint = scrollView.heightAnchor.constraint(equalTo: stack.heightAnchor)
constraint.priority = UILayoutPriority(rawValue: 999)
constraint.isActive = true

or make this lower than 750 ( the label's default vertical compression )

constraint.priority = UILayoutPriority(rawValue: 749)

the problem is that the label's vertical compression priority is lower than 999 which always make the stack equal to the scrollview height , hence no scroll

Pair answered 22/1, 2019 at 17:57 Comment(1)
Yes!! Lowering the constraints priority solved the issue. Thanks!Dillie
M
1

Have you looked into using a UICollectionView? It sounds like what you're trying to rebuild.

Make sure to set "Scrolling Enabled" to true and set the scroll direction.

Missal answered 22/1, 2019 at 17:48 Comment(1)
I haven't, I think a full collection view would be overkill for the problem I'm trying to solveDillie

© 2022 - 2024 — McMap. All rights reserved.