Multiple flexible height items - how to set the autolayout priorities? - ios

Say you have (for example) a table cell layout with more than one dynamic flexible-height items,
They are linked vertically one to the other in the obvious way.
It seems very difficult to make this work, and very undocumented.
You'd expect that: you set the compression resistance of all the expandable items to 751. But that doesn't work.
After random experimentation, it seems to me that surprisingly you have to do something like this:
have the compression/hugging of the overall view on 250 and 750,
then strangely enough, for the three text views in the example, the priorities have to
...sequentially increase...
And, I think you have to make one of them "one lower" than the overall view - in the example one of them would be 749.
It's difficult/impossible to find the exact "formula" to make it work consistently.
What the heck is the logic of this? Is it just a pure bug in iOS?
Has anyone found the "correct formula" for making a number of expandables work in a cell?
priorities for the three text views
priorities for the overall holder view
cheers

Your question is not very clear to me. However, here's a good recipe to stack text views (whether or not you use a stack view for constraining them doesn't matter):
1. Disable scrolling for all your UITextViews:
Reason: If scrolling is enabled, a UITextView does not have a defined intrinsicContentSize. See this answer. Without an instrinsic content size, the content hugging and compression resistance ("CHCR") priorities have no meaning or effect.
2. Give each view along one axis a different priority for CHCR:
More precisely: Give all views that are connected with constraints along one axis a different CHCR priority that are not constrained in size along that axis (i.e. that don't have a fixed width / height constraint).
In your particular example, your setup shown in the screenshots is a correct solution:
Each of the three text views has a different content hugging and a different compression resistance priority. As a general rule you should start with the default values (750 / 250) and only slightly increase or decrease them as you did.
In case you use a table view with self-sizing cells (by setting its estimatedItemSize), the CHCR priorities won't matter at runtime because the cell will automatically resize to accommodate enough space for all three text views. You just need to set those priorities to "silence" Interface Builder.
If you use fixed-height table view cells however, the CHCR priorities are quite important because they determine
which of the views will shrink first if there's not enough space inside the cell (it's the view with the lowest compression resistance priority) and
which of the views will expand if there's more space inside the cell than the subviews actually need (it's the view with the lowest content hugging priority).
The CHCR priorities of the superview ("MV") is irrelevant as it's usually just a container view that does not have a defined intrinsicContentSize either. Its size is defined by the inner and outer constraints you add.
For more information on the CHCR priorities, see the chapter Intrinsic Content Size in the Auto Layout Guide.

When I am playing with cells that have a variable height I use:
tableView.estimatedRowHeight = 20.0
tableView.rowHieght = UITableViewAutomaticDimension
These two cause the table view render its cells with the appropriate height. In order for the height to be computed though you should set the cell's data in tableView(cellForRowAt)

Related

NSLayoutContraints: How to position a bottom view below the higher of two labels?

I am working on an iOS 11+ app and would like to create a view like in this picture:
The two labels are positioned to work as columns of different height depending on the label content. The content of both labels is variable due to custom text entered by the user. Thus I cannot be sure which of the the two labels is higher at runtime and there is also no limit to the height.
How can I position the BottomView to have a margin 20px to the higher of the two columns / labels?
Both labels should only use the min. height necessary to show all their text. Thus giving both labels an equal height is no solution.
I tried to use vertical spacing greater than 20px to both labels but this leads (of course) to an Inequality Constraint Ambiguity.
Is it possible to solve this simple task with Autolayout only or do I have to check / set the sizes and margins manually in code?
You can add labels to stackView
One way to do this is to assign equal height constraint to both label. By doing this height of label will always be equal to large label (Label with more content).
You can do this by selecting both labels and adding equal height constraint as mentioned in screen short.
Result would be some think like below
The answer given as https://stackoverflow.com/a/57571805/341994 does work, but it is not very educational. "Use a stack view." Yes, but what is a stack view? It is a view that makes constraints for you. It is not magic. What the stack view does, you can do. An ordinary view surrounding the two labels and sizing itself to the longer of them would do fine. The stack view, as a stack view, is not doing anything special here. You can build the same thing just using constraints yourself.
(Actually, the surrounding view is not really needed either, but it probably makes things a bit more encapsulated, so let's go with that.)
Here's a version of your project running on my machine:
And with the other label made longer:
So how is that done? It's just ordinary constraints. Here's the storyboard:
Both labels have a greater-than-or-equal constraint (which happens to have a constant of 20) from their bottom to the bottom of the superview. All their other constraints are obvious and I won't describe them here.
Okay, but that is not quite enough. There remains an ambiguity, and Xcode will tell you so. Inequalities need to be resolved somehow. We need something on the far side of the inequality to aim at. The solution is one more constraint: the superview itself has a height constraint of 1 with a priority of 749. That is a low enough priority to permit the compression resistance of the labels to operate.
So the labels get their full height, and the superview tries to collapse as short as possible to 1, but it is prevented by the two inequalities: its bottom must be more than 20 points below the bottom of both labels. And so we get the desired result: the bottom of the superview ends up exactly 20 points below the bottom of the longest label.

How do I use a UIStackView in a non filling vertical capacity?

I want to stack varying height UIViews (each built in xibs) beneath the other (with a width that simply expands to meet the container) and I'm finding this hard probably because I don't understand it.
My situation is relatively simple. I have a dashboard view controller that has a vertically stacked UIStackView which should take 1 or more components that are dynamically added from code. The components are xib views that are each linked back to UIViewControllers which themselves are 'free form' views loaded dynamically. What I want is for them to be stacked as per their heights (not filled, or stretched or ratio filled but fixed to their intrisic heights) and with the appropriate space left at the bottom if space is available. With free form, the height itself seems to be ignored however...
As a result of them being xibs, it does not seem possible to set a 'maximum' (instrinsic) height within the view as only the controls within seem to have this as a property.
My views are based on uiviewcontrollers that I have added as follows:
override func viewDidLoad() {
super.viewDidLoad()
let component1 = Component1Presenter(nibName: "Component1View", bundle: nil);
self.addChildViewController(component1)
let component2 = Component2Presenter(nibName: "Component2View", bundle: nil);
self.addChildViewController(component2)
let component3 = Component3Presenter(nibName: "Component3View", bundle: nil);
self.addChildViewController(component3)
let component4 = Component4Presenter(nibName: "Component4View", bundle: nil);
self.addChildViewController(component4)
_dashboardComponentStack.addArrangedSubview(component1.view)
_dashboardComponentStack.addArrangedSubview(component2.view)
_dashboardComponentStack.addArrangedSubview(component3.view)
_dashboardComponentStack.addArrangedSubview(component4.view)
}
I've tried every variant of the UIStackView I think!, and in all cases they stretch or fill or exclude some of my views. My assumption is that the stack would take the intrinsic height of my views and stack them accordingly with the very last filling the remaining space. I'm clearly missing something obvious or very subtle or I'm just so very new to this to have missed the point. Just a reminder again, is this the right control for me to avoid stretching or filling a stacked set of views?
It is hard to give you a definitive answer, but here's an attempt at an explanation that may lead you in the right direction.
The stackview layout is based on constraints. You are correct that if the subviews all have intrinsic content sizes, then the stackview should be able to figure it out, or give you some error information, in the console.
Don't forget that you may need to manipulate the content compression resistance, or the content hugging priority, to resolve any ambiguity there may be about which view's intrinsic content size takes priority over the other's.
From the way you phrase it, it seems you might have misunderstood how stackviews distribute the subviews. It is not the last subview that fills any remaining space. How the space is filled is entirely based on the auto-layout properties if the subviews, such as intrinsic content size, content compression resistance, content hugging priority and any other constraints you may have wired between the subviews.
A view may be 'excluded' if there is not sufficient space to satisfy all the intrinsic content sizes. If you have not made it explicit how to handle that through constraints and/or setting content compression resistance, then the auto-layout engine will attempt to 'guess' which view is not important and therefore can be compressed, sometimes to the effect of entirely removing it.

How to implement a self sizing UITableView?

I want to dynamically resize my UITableView based on its content. As an UITableView has no intrinsic content size, I have to manipulate the height constraint programmatically. However, I want to have a max_height after which the UITableView stops growing.
With each reload, I set the UITableView's height constraint to its contentSize. If the contentSize is bigger than what fits into the defined boundaries, AutoLayout complains it "can not satisfy constraints simultaneously". So I tried giving the UITableView's height constraint the priority 999. In my understanding, if AutoLayout has to break constraints, it breaks the ones with the lowest priority first and tries to get as close to the constant as possible. (Perfect!)
If everything above the TableView is of fixed height, this works fine as seen in:
As soon as anything above the TableView relies on intrinsic contentSize (like a StackView with two labels) it doesn't work anymore. As soon as the height constraint gets too big, anything without a fixed height gets compressed. (Setting the compression resistance to 751 and content hugging priority to 249 does not change this)
To make it easier to verify I have created a GitHub project (https://github.com/Shanakor/SelfSizing-TableView/branches). On branch "master" you will find the working copy and on branch "not_working" you will find the alternative approach.
Thanks for your help.
Even after your change in Question description the reason is in constraints' priorities. Your stackview height is defined from its' intrinsic size. Which in turn is defined according to intrinsic sizes or height constraints of the arranged views (in this case it's intrinsic size of labels). So if you want your stack view to keep it's size (i.e. resist compression), the priority of the compression resistance(of the stack view and all its' arranged views) should be higher than the priority of table view height constraint.
So you need to set table view height constraint priority lower than UILayoutPriorityDefaultHigh (i.e. at least 749) and the default compression resistance for the stackView

Embedding three UILabels in a horizontal UIStackView gives different results in a table view

For a week I have been struggling with a 'simple' piece of layout in a storyboard. I want three labels which all have numberOfLines set to two.
The UIStackView has some constraints to position it in the table cell. The two left labels have a width constraint set to <= 100 to make sure they don't stretch out too far. I have been playing a lot with the content hugging and compression resistance and with things like setNeedsLayout or layoutIfNeeded. You can see the problem in the screenshot I added. There is barely any code written in the ViewController.
When you check out the test project I added and run it on the simulator you will notice when scrolling up and down the cells will all start to look the same and the text is no longer truncated. That's exactly what I want.
Here is a link to the test project I am working in.
In Prototype Cell set horizontal and vertical Content Hugging Priority to 1000 and the same for Content Compression Resistance Priority to 1000 (Left Label and Middle Label) and the result is what you are probably looking for, make Left and Middle labels as small as they can be and the right one to fill the gap...

Label's height issue in tableView with auto layout

I use autolayout. I am displaying 2 labels in custom UITableViewCell. Label1 is above Label2. Their text is dynamic.
The issue is the height of one of the labels when displayed is larger than its text.
I tried changing their Content Hugging Priority.
So what happens is, if that priority is same or Label1's priority is higher, then Label1 is having exact height to fit its text but Label2 has larger hight than required. And when Label2's hugging priority is higher than issue is with Label1's height.
Any idea how to solve it?
It looks like you are expecting your cells to auto-size based on auto-layout's constraint information, but UITableView sizes it's cells using frames/autosizing masks. If this is the expectation and you aren't autosizing the cells like in this question, then your labels are going to either force-clip themselves in order to fit inside the cells with the sizes they were given from the table view or grow to satisfy all of the margin-constraints.
Since the content hugging and content compression resistance priority values are less than required (less than 1000), they are considered "optional" and will be satisfied as close as they can be without violating any of the other required constraints. This is why your label begins growing (or clipping itself).
This can be solved in a couple of ways off the top of my head:
If you don't care about the cell having a variable height and are fine with the labels migrating toward the top of the cell, then make the constraint that pins the bottom label to the lower edge of the superview be non-required. More specifically, make that constraint have a priority lower than the vertical contentHuggingPriority for both of the labels. This way the content hugging priority constraints will take precedence over the lower constraint.
Make your cells auto-size themselves (using auto layout or otherwise) so that the system never has to consider the vertical contentHuggingPriority of each label 'optional'.
I solved this after experimenting with lot of things. The only thing I had to do is to set horizontal and vertical content compression resistance priority to required.i.e. 1000.
I did this for all labels because I don't want any of the labels to trim their content.
One more thing which is too much important is Getting Right Height Of Cell. If there is even 1pt of error in calculating custom cell's height it will not be displayed as expected.
Hint :
If height of any view is greater than expected then possibly calculated height of cell is greater than what is actually required.
If any of views is shrinking vertically or not displaying whole content then possibly calculated height of cell is lesser than what is actually required.
Yoy can test if height is wrong by adding/removing constant value to height (variable) you calculate for cell.

Resources