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)
}
}
heightAnchor
constraints. The first one is a range (vialessThanOrEqualToConstant
) 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