UIStackView not laying out as expected in a UIScrollView

I'm attempting to put a (vertical) UIStackView in a UIScrollView, but it doesn't seem to want to scroll. Instead it it squishing all the arrangedSubviews into the bounds of the scroll view.
autolayout-wise, I'm doing the following:
1. constraining the UIScrollView to its superview (top, height, leading and width)
2. add a UIView inside the scrollView label it Content View

3. constrain the top, bottom, leading, and trailing of the contentView to the scroll view

4. constrain contentView's width to match ScrollView's width

5. add a vertical UIStackView inside the ContentView

6. constrain its top, bottom, leading and trailing values to the contentView


attached is a screen shot link. I have posted a screen shot here: img.gg slash Py9T81y


At the top of the screen shot is a stackview that lays out as expected (not in a scrollView)

At the bottom is the stackview inside the scrollView that does not layout correctly


any/all guidance appreciated. (project/source code available if interested)


thanks in advance,

Mike

1. Try making the height of the scrollview more than height of it's container view.

What is the content that you want to scroll inside scrollView ?

- contentView

- stackView


You do not seem to set contentSize for the scrollView.


That is needed to have a scroll effect.

And content size must be larger than scroll.frame if you want some scrolling.


Here, depending on the answer abobe:

scrollView.contentSize = contentView.frame.size

or

scrollView.contentSize = stackView.frame.size


You may be creating too many constraints:


1. constraining the UIScrollView to its superview (top, height, leading and width)
2. add a UIView inside the scrollView label it Content View

3. constrain the top, bottom, leading, and trailing of the contentView to the scroll view

If you scroll the contentView, you should not constrain it to the scrollView. To have a scroll effect, contentView must be larger that ScrollView

4. constrain contentView's width to match ScrollView's width

Same remark as above in 3. In addition, if you have constrained all sizes, this is redundant

5. add a vertical UIStackView inside the ContentView

6. constrain its top, bottom, leading and trailing values to the contentView

Don't constrain the bottom



If you want to share code, please post an email address for a moment here.

I have assumed you want to scroll the contentView.

BTW: you could put directly the stackView in the ScrollView, without intermediate contentView, unless you need for other purpose.


Here is the code I put in viewController


import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var contentView: UIView! // Made its height bigger than scrollView's
   
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        scrollView.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
    }
   
    override func viewDidLayoutSubviews() {
       
        scrollView.contentSize = self.contentView.frame.size
    }

}


And the constraints I defined:


1. constraining the UIScrollView to its superview (top, height, leading and width)

2. add a UIView inside the scrollView label it Content View. Make its height greater than scrollView height

3. constrain the top, leading, and trailing of the contentView to the scroll view. Constrain height of contentView to its value.

4. Already achieved by 3 (leading & trailing)

5. add a vertical UIStackView inside the ContentView

6. constrain its top, leading and trailing values to the contentView

Don't constrain the bottom, that may distord the cells (probably the problem you have)

7. Set height of each cell in stackView to 50 (in your case, should be dynamic)

Hi Claude,

first thanks for the response, it's much appreciated.


1. you are correct, I was not setting contentSize. I added an override to viewDidLayoutSubviews() and set the scrollView's contentSize to the contentView's size. Sadly, putting a breakpoint here, it gets called only once, and the contentView's size is the 196 that the scrollView is constrained to.

overridefunc viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = scrolledStack.frame.size
    }

2. for readability, I removed the contentView that was a subView of the scrollView (and contained the stackView). The stackView now is directly the single subview of the scrollView.


The part that I didn't mention (and is possibly connected to this not working as I expect) is that the views in my stackView have content-dependent height. In a nutshell each of the arrangedSubviews in the stackView is a UIView that contains a UILabel. The UILabel has leading, trailing and top and bottom constraints, but no height constraint. My goal is for the label height to be either 1* lineHeight, or 2*lineHeight, depending on whether the string fits on one line, or requires 2 lines. (Analagous to myTableView.rowHeight = UITableView.AutomaticDimension)


and when the stackView is not in a scrollView, this dynamic height calculation is happening correctly. However when the stackView is in a scrollView some of the arrangedSubviews are being correctly sized based on the Label's content, but the others are getting squished. (to prevent the stackView's height from exceeding the scrollView's height.
I'm currently trying to use intrinsicContentSize of the views in the stackView to allow it to size correctly, but so far this isn't solving my problem. (eg when I set the height of the intrinsicContentSize to 100 the height of the arrangedSubviews in the stackview not in the scrollView. however the arrangedSubviews in the stackview in the scrollView are still mostly clipped)

again, thanks for any and all help with this.


Mike

hat the scrollView is constrained to.

For this first point, why do you constraint the size to 196 ?

That seem to be more or less the size of the scrollView.


Try setting contentSize to 400, just to see.

Later, you'll have to compute the precise size of the stackView, depending on cells content and spacing.


Could create a computed var

var stackHeight: CGFloat {
     var height : CGFloat = 0
     for view in scrolledStack.views {
          height += view.frame.height + scrolledStack.spacing
     }
     return height
}


overridefunc viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = stackHeight
    }

I was able to force the contentSize of the scrollView larger. When I attempted to scroll, the content did not move but the scroll bar behaved as if the content was scrolling.


grasping at straws, I removed the constraints that tied the contentView's top and bottom to the scroll view. This made it work.

I also needed to force a layoutIfNeeded() in viewDidLayoutSubviews()

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrolledStack.superview?.layoutIfNeeded()
        scrollView.contentSize = scrolledStack.frame.size
    }


very curious.


Not entirely sure why this solved the problem, as I thought the top and botton constraints of the contentView to the scrollView were serving some purpose.

Mike:


>Not entirely sure why this solved the problem, as I thought the top and botton constraints of the contentView to the scrollView were serving some purpose.


Child views inherit from parent, so those being removed/changed, coupled w/my suggestion above to increase the scrollview's height, would perhaps explain where you are now. I suspect your issue was no more complicated than that.


Thanks for the followup and good luck w/your apps.


Ken

I removed the constraints that tied the contentView's top and bottom to the scroll view. This made it work.

But you told you had removed contentView to have the stack directly in the view.

What configuration are you speaking of now ?


At least, you should not constrain contentView to bottom.

That's what I explained in a previous reply:

6. constrain its top, leading and trailing values to the contentView

Don't constrain the bottom, that may distord the cells (probably the problem you have)

> I removed the constraints that tied the contentView's top and bottom to the scroll view


Keep in mind - the scrollView is a 'window' into the content that is contained in the contentSize. That window scrolls around within the contentSize. If you constrain what is behind the window to fit in the size of the window then there is no scrolling.


And putting things in viewDidLayoutSubviews: may override constraints like autolayout that are embedded in xib/storyboard files and that can cancel scrolling.

If you have an object (image, stackView or whatever) and want to be able to scroll.


The object has a certain size, X * Y.

You put it into a ScrollView of size x * y ; one of the dimensions is smaller that object, otherwise there is nothing to scroll.


As PBK pointed out, the ScrollView, even though it the superview of the object is in fact a window to "move over" the object, revealing a part of it. But the window itself doesn't move, just what is displayed inside


The Scroll may move in the limits defined by contentSize: this defines the bounds of what can be made visible in the scrollView.


So: contentSize must at least be equal to the object size (may be larger), hence bigger than scrollView.frame.

You may constrain the scrollView to its superview (as safe area), but should not constrain the Object.

When you scroll, you change the offset of the contentOffset of scrollView.


Here is the usual illustration


A ___________________---

| OBJECT | i

| | i

| B _______ | i

| | Scroll | | i

| |_view__| | i

| __________________| i

i i

i ------------------------------- i contentSize


ContentOffset is AB (positive in the drawing above)

UIStackView not laying out as expected in a UIScrollView
 
 
Q