UIScrollView that expands with contents' instrinsic size until height X, and then scrolls
Asked Answered
M

7

23

I'm basically trying to reproduce the behavior of the title and message section of an alert.

enter image description here

The title and message labels appear to be in a scroll view. If the label text increases then the alert height also increases along with the intrinsic content size of the labels. But at a certain height, the alert height stops increasing and the title and message text become scrollable.

What I have read:

Articles

Stack Overflow

The answer may be in there but I was not able to abstract it.

What I have tried:

Only focusing on the scroll view with the two labels I tried to make a minimal example in which a parent view would resize according to the intrinsic height of the scrollview. I've played around with a lot of constraints. Here is one combo (among many) that doesn't work:

enter image description here

I've worked with auto layout and normal constraints and even intrinsic content sizes. Also, I do know how to get a basic scroll view working with auto layout. However, I've never done anything with priorities and content hugging and compression resistance. From the reading I've done, I have a superficial understanding of their meanings, but I am at a loss of how to apply them in this instance. My guess is I need to do something with content hugging and priorities.

Musculature answered 18/6, 2016 at 12:50 Comment(4)
scroll views don't have an intrinsic size - have you subclassed it?Osborn
@Wain. I haven't subclassed the scroll view, but since UILabel does have an intrinsic content size, I am trying to get the content view of the scroll view to increase in size up to a point based on the size of the labels it contains.Musculature
it should if you pin the edges, but that just means you haven't explained your question very well...Osborn
@Wain, sorry, my last comment was incorrect. The content view size is increasing. I am trying the get the scrollview's parent (and with it the scroll view itself) to increase in size as the content view's size increases. When the parent reaches a certain max size, any increase in the size of the scroll view's content view causes normal scrolling behavior. I'm beginning to think this is only possible by manually changing the constraints at run time.Musculature
R
18

I think I have achieved an effect similar to the one you wanted with pure Auto Layout.

THE STRUCTURE

First, let me show you my structure:

enter image description here

Content View is the view that has the white background, Caller View and Bottom View have a fixed height. Bottom View has your button, Caller View has your title.

THE SOLUTION

So, after setting the basic constraints (note that the view inside scroll view has top, left, right and bottom to the scroll view AND an equal width) the problem is that the scroll view doesn't know what size should have. So here comes what I have done:

I wanted that the scroll could grow until a max. So I added a proportional height to the superview that sets that max:

enter image description here

However, this brings two problems: Scroll View still doesn't know what height should have and now you can resize and the scroll view will pass the size of his content (if the content is smaller than the max size).

So, to solve both issues I have added an equal height with a smaller priority from the View inside of the Scroll View and the Scroll View

enter image description here

I hope this can help you out.

Rockwell answered 15/9, 2016 at 16:50 Comment(5)
This looks intriguing. Did you actually get something working? I'd be glad to mark this as the new solution, but I'm currently working on other projects so I don't have the time to do a lot of testing. This is something I want to come back to, though.Musculature
@Musculature Test it when you can, I have time :p. I posted as soon as I tested the basics, but I will keep using this solution and increase its complexity. I can keep the feedback going :)Rockwell
My mind is having a hard time grasping how this is working, but since you have confirmed that the basics work, I will mark this as the new accepted answer. Any other other details or future improvements that you want to edit into your answer are welcome.Musculature
In my tests this only works if the content of the UIScrollView (Label etc.) will no be bigger than the max height of the UIScrollView. Just add a very long text which makes the View growing over the max height which was defined with the first constraint - 65% of superview. However, in this case the text will be cut off...Bonina
To make it scroll, instead of setting the constraint (equal height with a smaller priority from the View inside of the Scroll View and the Scroll View) to priority 999, set it to 249. This works because priority 249 is lower than the priority of intrinsic content size. In this case, the Scroll View's height will grow according to the View inside it but caps at the max. Since the priority is 249, the View inside the Scroll View can continue to grow even when Scroll View's max is reached, thus increasing the contentSize of the Scroll View making it scrollableTriolet
O
15

Your problem can't be solved with constraints alone, you have to use some code. That's because the scroll view doesn't have an intrinsic content size.

So, create a subclass of scroll view. Maybe give it a property or a delegate or something to tell it what its maximum height should be.

In the subclass, invalidate the intrinsic content size whenever the content size changes, and calculate the new intrinsic size as the minimum of the content size and the maximum allowed size.

Once the scroll view has an intrinsic size your constraints between it and its super view will actually do something meaningful for your problem.

Osborn answered 24/6, 2016 at 11:24 Comment(4)
All right. That's too bad that it requires subclassing, but at least I don't feel so bad that I couldn't get a pure auto layout solution to work. So you think the standard alert controller uses a UIScrollView subclass?Musculature
It's a guess, there are other ways to do it, but the point is that autolayout can't do it without more information / logic being applied.Osborn
I'm changing the accepted answer since it turns out that an auto layout only answer is possible.Musculature
After spending about a day figuring out contentSize I concluded that calculating the contentSize in viewDidLayoutSubviews() and setting it there, in code, seems to work. contentSize badly wants to be (0,0) as the Apple docs note.Kepler
A
4

It can be done in Interface builder using auto layout without any difficulties.

  1. set outer container view ("Parent container for scrollview" in your sample) height constraint with "less than or equal" relation.

2.add "equal heights" constraint to content view and scroll view. Then set low priority for this constraint.

That's all. Now your scrollview will be resized by height if content height changed, but only to max height limited by outer view height constraint.

enter image description here enter image description here enter image description here

Annoy answered 11/8, 2017 at 13:28 Comment(1)
I lost so much time today. Hours lost without any result until I found your answer. Thank you! Commending you.Charles
R
1

You should be able to achieve this solution via pure autolayout.

Typically if I want labels to grow as their content grows vertically I do this

[label setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; 
[label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];

In order for your scrollview to comply to your requirements you will need to make sure a line can be drawn connecting the top of the scrollview all the way through the labels to the bottom of the scrollview so it can calculate it's height. In order for the scrollview to confine to it's parent you can set a height constraint with a multiplier of the superview of say 0.8

Realism answered 26/6, 2016 at 22:27 Comment(3)
I've been able to get labels to grow vertically with their content without changing the content hugging and compression resistance. Why do you set them?Musculature
That solution uses preferredMaxLayoutWidth. When I don't set that and use left/right constraints to superview or have it nested in a view with variable height, my labels don't wrap without it. If you already have your labels growing. Setting the scrollview height to be less than or equal to the superview multiplier should solve your issue.Realism
I'm working on getting your answer to work. Can you provide any example details for content hugging and compression resistance?Musculature
P
1

You can do this fairly simply with two constraints

  • Constraint 1: ScrollView.height <= MAX_SIZE. Priority = Required
  • Constraint 2: ScrollView.height = ScrollView.contentSize.height. Priority = DefaultHigh

AutoLayout will 'try' to keep the scrollView to the contentSize, but will 'give up' when it matches the max height and will stop there.

the only tricky part is setting the height for Constraint 2.

When my UIStackView is in a UIViewController, I do that in viewWillLayoutSubviews

If you're subclassing UIScrollView to achieve this, you could do it in updateConstraints

something like

override func viewWillLayoutSubviews() {

    scrollViewHeightConstraint?.constant = scrollView.contentSize.height     

    super.viewWillLayoutSubviews()
}
Phagocyte answered 28/10, 2019 at 19:18 Comment(0)
W
0

To my project, I have a similar problem. You can using the following way to make it work around.

First, Title and bottom action height are fixed. Content has variable height. You can add it the mainView as one child using the font-size, then call layoutIfNeeded, then its height can be calculated and saved as XX. Then removed it from mainView.

Second, using normal constraint to layout the content part with scrollView, mainView has a height constraint of XX and setContentCompressionResistancePriority(.defaultLow, for: .vertical).

Finally, alert can show exact size when short content and show limited size when long size with scrolling.

Weaken answered 1/4, 2019 at 9:13 Comment(0)
S
0

I have been able to achieve this exact behavior with only AutoLayout constraints. Here is a generic demo of how to do it: It can be applied to your view hierarchy as you see fit.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let kTestContentHeight: CGFloat = 1200

        // Subview that will shrink to fit content and expand up to 50% of the view controller's height
        let modalView = UIView()
        modalView.backgroundColor = UIColor.gray

        // Scroll view that will facilitate scrolling if the content > 50% of view controller's height
        let scrollView = UIScrollView()
        scrollView.backgroundColor = .yellow

        // Content which has an intrinsic height
        let contentView = UIView()
        contentView.backgroundColor = .green


        // add modal view
        view.addSubview(modalView)
        modalView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([modalView.leftAnchor.constraint(equalTo: view.leftAnchor),
                                     modalView.rightAnchor.constraint(equalTo: view.rightAnchor),
                                     modalView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor,
                                                                       multiplier: 0.5),
                                     modalView.bottomAnchor.constraint(equalTo: view.bottomAnchor)])

        let expandHeight = modalView.heightAnchor.constraint(equalTo: view.heightAnchor)
        expandHeight.priority = UILayoutPriority.defaultLow
        expandHeight.isActive = true

        // add scrollview to modal view
        modalView.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([scrollView.topAnchor.constraint(equalTo: modalView.topAnchor),
                                     scrollView.leftAnchor.constraint(equalTo: modalView.leftAnchor),
                                     scrollView.rightAnchor.constraint(equalTo: modalView.rightAnchor),
                                     scrollView.bottomAnchor.constraint(equalTo: modalView.bottomAnchor)])


        // add content to scrollview
        scrollView.addSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
                                     contentView.widthAnchor.constraint(equalTo: modalView.widthAnchor),
                                     contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
                                     contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
                                     contentView.heightAnchor.constraint(equalToConstant: kTestContentHeight)])
        let contentBottom = contentView.bottomAnchor.constraint(equalTo: modalView.bottomAnchor)
        contentBottom.priority = .defaultLow
        contentBottom.isActive = true
    }

}
Serendipity answered 6/9, 2019 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.