Xcode: constraining varying height UILabels to fixed size container - ios

I’m building an iOS app which includes a quiz. Questions are displayed in the upper portion of the screen (see below). There are always five possible answers. The thing is: the answers are procedurally generated and vary in length, which leads to line breaks in the label sometimes.
This is the current state
Maybe it’s hard to tell from the picture, but the spacing between the first and second and then between the second and third is not the same as I intend it to be.
This is what I had in mind
Essentially, I’d like the top- and bottommost labels to have the same space towards the container. The labels in the middle should all have the same spacing between each other, but should also adapt, if one of the labels gets bigger (when the size of a text is longer than the width and a line break occurs).
To achieve this, I tried the following:
Organise the labels as a stack view:
Nearly worked, the only problem I have here is, that the size of the
labels is calculated after the stack view is displayed, which leads to wrong constraints/paddings/margins being applied to the (possibly) longer texts at runtime.
Organise the labels with regular constraints
I tried setting the
priority of the constraints for the middle labels lower than
that of the top- and bottommost ones, so those would be the ones
to resize, if the labels enlarges, but it appears that at
runtime one of those is chosen to shrink, whereas the others
remain at their default size.
I'd really appreciate if you could help me out, constraints always seem to be a pain in my neck...

Agreed... I think UIStackView works fine, just have to set it up properly.
Also you may need to call .sizeToFit on the labels when you set the text.
I put up an example for you - the sizing is not exact, but you should be able to follow the technique...
https://github.com/DonMag/StackViewFun

I think the best practice in your case would indeed be to use a UIStackView. Then you have multiple option in solving this problem:
You can set the distribution of the UIStackView to Fill Proportionally and set a Minimum Font Size or Minimum Font Scale to your UILabels.
You can also set the distribution of the UIStackView to Fill and then manually specify the height of your containers. The UILabels should still have some Minimum Font Size or Minimum Font Scale.
EDIT:
I just realized you want to keep all UILabels to have the same font sizes. Then a possible solution would be to embed the UIStackView into a UIScrollView and constraint the UIStackView's Leading, Trailing, Top and Bottom constraints to the ones of the ContentView of the UIScrollView. In this way the height of the UIScrollView will adapt in accordance of the needed height by the UIStackView.
Hope this will help you!

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.

Hide arrangedSubviews in UIStackView instead of clipping contents

I have a UIStackView with three labels whose height is determined using Dynamic Type and text that have can wildly varying lengths. The container for the stack view has a fixed width and height depending on device screen size (small on iPhone SE, for example.) I want to center the stack view within the container (with some outer margins.)
The problem is that depending on the font size and container height, some of the labels in the stack view will be clipped. Here is an example with the third label:
I have experimented with layout constraint priorities for both the stack view and the labels, but this doesn't appear to be the right approach. Instead setting the visibility of the labels works better: correct spacing between elements is maintained.
My question is then what is the right time to detect that the label's height isn't fully displayed and to hide it.
The label height is close to, but not exactly equal to the UIFont's lineHeight so there's some rounding involved that makes this a little difficult.
The biggest problem is that after a layout pass in the UIStackView's layoutSubviews the heights of the arranged subviews can be detected, but you can't hide the arranged views at that point because it causes another layout pass and recursion.
So what am I missing? :-)
Here's a test project - build for iPhone Xs in the Simulator and you'll see the same results in the screenshot above.
Solution
Tom Irving's gist below pointed me in the right direction. The trick is to enumerate the subviews after a layout pass and remove them if height requirements aren't met.
The updated project shows how to do this in DebugStackView's layoutSubviews. And yes, UIStackView is a worthy adversary.
Could you act on viewDidLoad?
My intuition would be to add up the height of all visible subviews in the stack view and then hide the last if there's a problem.
In the sample you've provided I would recommend getting a CGSize with [self.firstLabel textRectForBounds:self.view.bounds limitedToNumberOfLines:0] for each visible label, making sure to take the margin between items into acount, and determining if the total height is greater than the constant height you've assigned to the stack view. If so, hide the elements that go beyond the stack view's height.
Of course, there might be more to the problem than I understand, but that would allow you to calculate before the layoutSubview pass happens.

Using Auto-Layout in custom UITableViewCell

I'm working with a custom UIableViewCell that has four UILabels inside of it.
What I'm currently struggling with is that for smaller iPhones, I'm able to get the correct portrait layout (the numbers correspond to the label numbers).
What I'm trying to do for larger iPhones (screenshot below is from a 6SPlus) is have label 3 move in the direction of the arrow:
I have three constraints for the third label. My thought was to have one of them set with a priority of 1000 to be the "base constraint", and another, set at 999, to change the spacing of the cell:
However, it doesn't seem to have any effect. How can I alter the position of the third label using constraints based on the size class? Thanks!
EDIT: This is an approximate illustration of what I'm trying to get to:
Use a UIStackView and put your labels inside it. The stack view can be configured to distribute evenly your four labels, putting the same amount of space between them regardless of how the width of the table grows or shrinks as it launches on a bigger or smaller phone. Thus, you'll look great everywhere.
(If you're not able to use UIStackView, you can achieve the same effect of even distribution using invisible spacer views.)

Universal layout constraints

I have an app which I've designed in the width:any height:any universal size template in XCode (Swift). Basically, I have a custom cell, with its own uitableviewcell class too, and two labels inside of it which display array data. Anyway, I am struggling to add the correct restraints etc, so that the labels and text inside them display properly on all iOS devices. Even though I have changed the font to be able to go to 0.2 scale, there's still sometimes overlap or text clipped in the label on smaller iOS screens. It's probably a simple thing to resolve. So can anyone help me make this layout look the same, or the equivalent, on all iOS devices and all orientations too please? Here is my design:
The fact that your labels are overlapping each other is probably that you don't have the proper width constraints.
This is what I will do, taking the left label as an example:
add constraints to the left, top, bottom margin
add a width constraint to the parent view and making it 50% of the parent width
You will need to do the similar for the right label replacing the left margin constraint with a right margin one.
Now both the labels only take 50% of the width, then they will not overlap any more guaranteed.

When do you need content hugging for labels and buttons to prevent them from taking the space of a text field?

I am puzzled with a simple layout created programmatically, where you have a UILabel, a UITextField and a UIButton in a row using constraints. I hope you can help me understand the following behaviour.
If I use visual format language to lay out those views like this...
|-[label]-[field]-[button]-|
...I see that the label wants to take as much space as possible, like this:
[ label ] [field] [button]
But if I remove the label from the equation...
|-[field]-[button]-|
...then it's the button who wants to take over the space:
[field] [ button ]
By setting a "high hugging priority" to the label and button I can control their size (they keep their intrinsic size, I guess). But I don't know why the differences in behaviour in these cases.
Do you know how is autolayout exactly working here?
Related question:
Using Auto Layout to have UILabel and UITextField next to each other
When the content-hugging priorities of the label and the button are equal to each other, and one of them needs to be stretched to satisfy other constraints, then it is arbitrary which is stretched. In general, when there are multiple solutions to the constraints and the priorities don't distinguish them, there is ambiguity and the auto layout system can resolve the constraints in any of the possible solutions. It can change from run to run. It can even change each time the layout pass is done, which means views can jump around while the user interacts with them.
The fact that in one case the label was stretched and in the other the button was is just random.
When you have something like |-[label]-[field]-[button]-| and there's any chance that the width of the container will not always equal the sum of the intrinsic widths of the three views and the spacing between them (so that something may need to be stretched), you should always designate one to be stretched by making its content-hugging priority lowest.

Resources