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

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.

Related

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.

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

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)

Complex AutoLayout for Cell with dynamic size

I have a cell that contains a container with 10 subviews (two of them are simply bounds and the others are labels). The scheme looks like this.
Dynamic labels may contain huge text so the cells should conform the appropriate size to fit the content. The question is how to set up all the constraints manually... I've tried a dozen of times to do it myself but seems I'm not that good at this. The table view supports auto dimension for row height and uses custom estimated height.
In Storyboard it looks this way.
Where blue views are a subviews of View C. A grey view behind is a View B. Bold labels are static and the others are dynamic.
Demo project.
How to setup constraints?
Thank you very much in advance!
I managed to setup your constraints so that you get the result you needed. This is what I get:
I hope this is how you wanted it to look like.
Here is a link with the project.
I will try to explain how I added the constraints so that it makes more sense.
First of all, you have view B which needs to be as big as the contentView. For this you add top/bottom/left/right constraints to the superView. Because you are using automatic dimensions, if you add all constraints with priority 1000(the maximum one), you will get some error with the constraints while running. This is because, before the cell size can be calculated automatically it is zero, so the constraints crash. Therefore, I set the priority for top and trailing space with a priority of 999 so that you don't see the error log anymore. The result is the same.
Then views C needs (top or bottom)/left/right and height constraint
Then you need to add the constraints for the labels. Since you need the right ones to have multiple lines, the constraints need to specify the vertical layout for this particular case. So, you have as follows: first label: top/left to name label and right to super view. All the other have top to the previous one,and bottom to the next one.
for the labels that don't need to resize you just need leading space to parent,horizontal space to the right label and static width. Also, you will need a constraint to align the top with the label on the right.
This is the result I get:
Hope my explanation made sense, just let me know if you have questions. Good luck with your project!

Auto Layout without explicit width constraints and with greater-or-equal trailing space misbehaving

I've got a scroll view contained directly inside the content view of a view controller, at full size in both dimensions. The top, bottom, leading and trailing space constraints from the scroll view to its superview (horizontal) and the layout guides (vertical) are all set to 0. The VC is eventually meant to be nested as a child view controller in one or two places. I'm using a Storyboard.
I've placed a number of elements inside the scroll view and constrained them to it, but I'm seeing various kinds of strange behavior. Below is a screenshot with all the subviews of the scroll view selected to display their constraints. The scroll view's four constraints to the top level view are not visible in it. The view controller has been set to Freeform size, with its top level view (and, accordingly, the scroll view's content view) 616 pts high, guaranteeing that scrolling will be necessary at runtime.
Before analyzing the screenshot, here's a list of things that I'm trying to achieve.
The vertical spacing between elements is set by the designer and fixed. (BTW, none of the vertical constraints, text styles etc. in this wireframe are final yet; the whole image is for illustrative purposes only.)
All the labels (except the topmost one) should start at their intrinsic size, expand up to the width of the scroll view (minus the standard HIG horizontal space of 20 pts on both sides).
Buttons are unlikely to be much bigger than this, but in case of localization surprises, we want them to behave just like the labels. (There's an extra vertical ≥ constraint on "Another Button"; it's irrelevant to this question.)
The web view has a fixed height, and its width should be determined by the width of the scroll view; standard 20 pt horizontal space on both sides.
The text views have a minimum height (67 pts here), but they should expand vertically if the contained text is too big to fit. None of them are editable or scrollable. Like the web view, they're horizontally spaced the standard 20 pts apart from the leading and trailing edges.
As you can see, none of the elements have explicit width constraints. The whole thing relies on the leading and trailing space constraints between the elements and the scroll view. The layout, in my mind, would somewhat gracefully work on hypothetical wider-than-320 pt iPhones of the future without changes to the constraints. It would also work after rotating to landscape orientation (it might look a bit silly, but it would work).
I'll go through the points step by step, referring to the screenshot where necessary.
1: This works, nothing out of the ordinary here.
2: The leading constraints of the labels are all simple Equal 20 pt standard spaces. The trailing constraints are Greater Than or Equal 20 pt standard, ostensibly to allow them to grow to be scrollView.frame.size.width-40 wide, but no wider.
3: Same as 2.
4 and 5: Here's where it gets interesting. The web view and the text views are all listed as Misplaced Views, with IB saying their frames will be different at runtime. The orange dashed borders denoting the correct frames only reach horizontally as far as the longest element with a Greater Than Or Equal trailing constraint; here, it's "A Button With A Long Title", whose right edge is where the dashed border edges end.
Constructing this set of views and their constraints, I expected to have some trouble. I knew it would be tricky to have UITextViews that grow vertically taller than the ≥ 67 height defined here, perhaps only possible through code. Getting the labels and buttons to work as specified above through IB alone seemed a bit iffy, too.
What I didn't expect was the web and text views' reported correct frame being only as wide as the widest label or button. It seems that with this setup, the scroll view won't actually be 320 pts wide, but rather only as wide as necessary to fit the longest element and its spacing, and the web and text views are expected to comply. Given that the scroll view is firmly constrained on all sides to the top level view, which is set to be 320 pts wide, I have no idea why this is. SOMETHING must obviously define the initial width of the scroll view, but why aren't the constraints I've made from the scroll view to the top level view doing that?
Given the specifications above for this set of views, what do I need to change to make it happen?
This case demonstrates the fact that I truly do not properly understand Auto Layout yet, and I hope that the answers will enlighten me about many of its crucial aspects.
With respect to Xcode's warnings about misplaced views, select the view controller in storyboard, tap the "Resolve Auto Layout Issues" button in the lower-right-hand corner of the canvas (it looks like a Tie Fighter from Star Wars), then select "Update All Frames in View Controller". This forces all of your views to reflect their constraints.
Using Auto Layout with UIScrollView is a different animal; so much so that Apple felt it necessary to release a Technical Note on the issue: https://developer.apple.com/library/ios/technotes/tn2154/_index.html
When you connect all those constraints between subviews and the inside walls of a scroll view, the result is probably not what you expect. When you pin a subview to the sides of a scroll view, you are not in fact determining the subview's position relative to the scroll view. Instead, you are determining the scroll view's contentSize. This is weird.
You are using Apple's so-called pure Auto Layout approach from the Technical Note. With this approach, the subviews' constraints must dictate the scroll view's contentSize.
Let's take just one of your subviews and ignore all the rest, say one of the text views. And let's only think about that text view along the horizontal axis. If you wanted the text view to be constrained by the width of the scroll view without any horizontal scrolling, you would need to install a fixed-width constraint on the text view that was exactly the width of the scroll view's bounds minus the spacers. After doing this, the content size width would be the sum of the left spacer, the width of the text view, and the right spacer.
Unfortunately, you cannot install a constraint that establishes a relationship between the width of the text view and the width of the scroll view's bounds. And that's really too bad.
I don't actually recommend installing a fixed-width constraint on the text view. Instead, I would start over and use Apple's "mixed approach" from the Technical Note.
With the mixed approach, the subviews' constraints don't determine the scroll view's contentSize. Instead, you must explicitly set the scroll view's contentSize and the frame of a container view (i.e., a UIView content view).
Let's go back to that UITextView and the horizontal axis. Using the mixed approach, you could leave the constraints for the text view as they are (i.e., no fixed-width constraint). You could explicitly set the width of the scroll view's contentSize and the width of content view's frame as early as viewDidLoad. You could explicitly set these values to self.view.bounds.size.width because your scroll view hugs the sides of the main view.
To implement the mixed approach, you will have to instantiate the content view (UIView) in code and not set its translatesAutoresizingMaskIntoConstraints property to NO. By extension, you'll probably need to create your constraints for all those subviews in code as well (I don't know of any way around this). The visual formatting strings are tedious and repetitive, but they're actually easier to work with than constraints created in IB when configuring complex layouts (your layout is sufficiently complex).
I used the mixed approach to solve a SO challenge here: https://stackoverflow.com/a/21770179/1239263. Unfortunately, the solution hasn't been vetted yet. It's always possible that I'm nuts and I don't know what I'm talking about.

resize superview after subviews change dynamically using autolayout

I cant for the love of god the the hang of this resizing superview.
I have a UIView *superview with 4 UILabels. 2 function as header for the 2 others.
The content in all 4 are dynamic coming from database.
SizeToFit vs SizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize.
I use autolayout programatically and have set the superview height to be equal or greater to a dummy number.
where and how do I use these SizeToFit vs sizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize. I have read a lot of tips here on stack but ended up with nothing.
DO I need to recalculate the constraints for the superview somewhere specific. Maby setting the height to be ´#property` in its controller class and remove and readd it?
Atm I have tried to put everything everywhere and then some. Still I get the same size end result with the dummy height and text floating outside. Even after setting clipsToBound on subview.
I am scratching my hair of.. help
If you're using Auto Layout, here's what you need to do:
Make sure you aren't adding fixed width and/or height constraints to any of your subviews (depending on which dimension(s) you want to dynamically size). The idea is to let the intrinsic content size of each subview determine the subview's height. UILabels come with 4 automatic implicit constraints which will (with less than Required priority) attempt to keep the label's frame at the exact size required to fit all the text inside.
Make sure that the edges of each label are connected rigidly (with Required priority constraints) to the edges of each other and their superview. You want to make sure that if you imagine one of the labels growing in size, this would force the other labels to make room for it and most importantly force the superview to expand as well.
Only add constraints to the superview to set its position, not size (at least, not for the dimension(s) you want it to size dynamically). Remember that if you set the internal constraints up correctly, its size will be determined by the sizes of all the subviews, since its edges are connected to theirs in some fashion.
That's it. You don't need to call sizeToFit or systemLayoutSizeFittingSize: to get this to work, just load your views and set the text and that should be it. The system layout engine will do the calculations for you to solve your constraints. (If anything, you might need to call setNeedsLayout on the superview...but this shouldn't be required.)
Use container views
In the following example I have a 30x30 image, and the UILabel is smaller than the containing view with the placeholder text. I needed the containing view to be at least as big as the image, but it needed to grow to contain multi-line text.
In visual format the inner container looks like this:
H:|-(15.0)-[image(30.0)]-(15.0)-[label]-(15.0)-|
V:|[image(30.0)]|
V:|[label(>=30.0)]|
Then, set the containing view to match the height of the label. Now the containing view will ride the size of the label.
As #smileyborg pointed out in his answer, connecting the content rigidly to the superview informs the layout engine that the simple container view should cause it to grow.
Yellow alignment rectangles
If you want the yellow alignment rectangles add -UIViewShowAlignmentRects YES in your scheme's list of run arguments.
This almost follows #smileyborg answer and comes with a concrete example.
Won't describe all constraints, but those related to the calculation of the height of UI objects.
[Label] Labels must not have a fixed height constraint, in this case, AutoLayout won't resize labels to fit the text, so setting edge constraints is the key. (green arrows)
[Subview] Steps 1 and 3 are very easy to follow, but this step can be misunderstood. As in the case with labels, subviews must not have height constraint set. All subviews must have top constraint set, ignoring bottom constraint, which can make you think will trigger unsatisfied constraint exception at runtime, but it won't if you set bottom constraint for the last subview. Missing to do so will blow the layout. (red arrows)
[Superview] Set all constraints the way you need, but pay big attention to the
height constraint. Assign it a random value, but make it optional, AutoLayout will set the height exactly to fit the subviews. (blue arrows)
This works perfectly, there is no need to call any additional system-layout update methods.
This was made dramatically easier with the introduction of Stack Views in iOS 9. Use a stack view inside your view to contain all your content that resizes, and then simply call
view.setNeedsUpdateConstraints()
view.updateConstraintsIfNeeded()
view.setNeedsLayout()
view.layoutIfNeeded()
after changing your content. Then you can get your new size by calling
view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
if you ever need to calculate the exact size required for a view.

Resources