Force UIView with zero height to lay out its subviews - ios

I have an UIView with some labels in it. The labels have dynamic height depending on the text, and they have vertical spacing constraints to the parent view and to each other, giving the parent view its height. The constraint between the bottom of the last label and the bottom of the parent view is set to have a lower priority.
Additionally, the parent view has a height constraint that I set to 0 in viewDidLoad. This collapses the view and pushes all the labels down, since the last bottom constraint has a lower priority. I then have a button where I toggle the height constraint of the parent view, which opens/closes it:
#IBAction func viewInfoButtonTapped(_ sender: Any) {
viewInfoButton.isSelected = !viewInfoButton.isSelected
self.infoHeightConstraint.isActive = !self.viewInfoButton.isSelected
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
My issue is that the dynamic height of the labels are not set until I expand the parent view the first time, so instead of an animation where the labels are simply revealed, I get an effect where thy are revealed and grow in height at the same time. After the first expansion, the heights have been set, and subsequent expansions give the correct effect.
How can I force the view to lay out the constraints correctly before it is expanded the first time?
I have tried calling setNeedsLayout() and layoutIfNeeded(), but it doesn't seem to have an effect.

Set the Vertical Content Compression Resistance priority to Required (1000) on each of your labels:
Or, since you're likely looping through them at run-time to set the text in each, you can do it via code:
label.text = someString
label.setContentCompressionResistancePriority(.required, for: .vertical)
That should fix the initial "stretching" that you don't want.
If you're still seeing stretching (shouldn't, but just in case) you can also call sizeToFit():
label.text = someString
label.setContentCompressionResistancePriority(.required, for: .vertical)
label.sizeToFit()

Related

Autolayout UITableView expand parent view but not greater than safe area

I have a Storyboard which has a simple view hierarchy:
- View (has top/bottom constraints relative to safeArea >= 30, centerY)
- Label (has height/top(SuperView)/bottom(TextField) constraints)
- TextField (has height/top(Label)/bottom(TableView) constraints)
- TableView (has height >= 0, top(TextField)/bottom(Superview) constraints)
Inside the UITableViewDelegate (cellForRowAt):
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
The behaviour I wish to achieve is to have the TableView and the parent View grow as new records are added to it. However, the parent view should not exceed its top/bottom constraints relative to the safe area.
All the elements have height constrains and spacing explicitly set, except for the table view (which has height >= 0). As well, the parent's view content hugging priority is 250 and the tableview compression resistance is 750. I thought that fixing the height constraints and spacing between elements would allow the tableview to grow up to some point because the content compression resistance is higher for the top/bottom safe area constraints than it is for the TableView.
However, XCode is forcing me to set a height or a Y position constraint for the parent view. I can't do that because then the view cannot grow automatically.
I would prefer to stick with AutoLayout and wondering if anyone has an idea or resource on how to do this.
Table views do not automatically set their height based on the number of rows.
You can use this custom table view class (from https://stackoverflow.com/a/48623673/6257435):
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
This will "auto-size" your table view whenever the content size changes. To use it, add a table view and set its class to ContentSizedTableView.
Constrain the top, leading and trailing as desired.
Constrain the bottom to >= 0 to the bottom of the superview (or >= 8 if you want it to stop 8-pts from the bottom, or whatever value you want for your layout).
Give it a height constraint - doesn't really matter what, but using a value such as 100 let's you see it as you work with the layout.
Then edit that height constraint and check the Placeholder - Remove at build time checkbox.
The table view's height constraint will be removed when you run the app, and the ContentSizedTableView will automatically grow until it reaches the bottom constraint.

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

I'm basically trying to reproduce the behavior of the title and message section of an alert.
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
Auto Layout Magic: Content Sizing Priorities
Editing Auto Layout Constraints (documentation)
A Fixed Width Dynamic Height ScrollView in AutoLayout
Using UIScrollView with Auto Layout in iOS
Stack Overflow
Adding priority to layout constraints
Inequality Constraint Ambiguity
UIScrollView Scrollable Content Size Ambiguity
Ambiguity with two inequality constraints
IOS scrollview ambiguous scrollable content height in case of autolayout
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:
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.
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:
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:
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
I hope this can help you out.
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.
It can be done in Interface builder using auto layout without any difficulties.
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.
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
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()
}
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.
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
}
}

UIButton size not updating automatically after updating button image, using autolayout

I have a UIButton which is constrained with leading, trailing and center vertically constraints. The label on the left of the button grows until the trailing constraint of the button (greater than constraint) reaches the limit at which point the button does not move any closer to the switch and the label text begins to truncate.
There is a case where the button is set to hidden. This creates an extra gap between the label and switch when there is long text in the label. I am setting the image of the button to nil and expect it to resize based on the constraints but it never does.
Here is the code that attempts to force the resizing:
func hideInfoButon(hide: Bool) {
infoButton.hidden = hide
if hide {
infoButton.setImage(nil, forState: .Normal)
} else {
infoButton.setImage(UIImage(named: "icn_info_gry"), forState: .Normal)
}
setNeedsUpdateConstraints()
setNeedsLayout()
}
I have confirmed that the hide method is properly called and the image is actually being set to nil. I confirmed by not hiding the button and giving it a background color to make it visible.
Is there a solution which does not require me to add a width constraint to the button?
The content is in a tableViewCell for reference.
1. Add Constraints to label
2. Add Constraints to Info Button
3. Info Button trailing space must be standard spacing
4. Create Info Button Width IBOutlet Like
5. And when you wnat to hide button set width constraints 0
infoButtonWidth.constant=0
A hidden button will still occupy space. You could alternatively remove the button rather than hiding it...
infoButton.removeFromSuperview()
If you do this, you will probably also need an extra constraint between the label and the switch for when the button is removed. This constraint should have a lower priority so that it does not conflict when the button is visible.
EDIT:
Thinking about it further, this may cause headaches when cells are reused between rows. You may find it easier to have an IBOutlet for the button's width constraint, and just set it to zero to hide.

How to hide inner view completely when superview height constant is set to zero

As you can see, I have a UIView(red color) that has constraints - leading, bottom, width and height. All set to default(1000) priorities except height(999). Inside this UIView are three UIButtons those have constraints - leading, top, width and height.
All set to default(1000) priorities except height (2). So that superview's height can override the innerviews height.
At the click of orange button. I set UIView's height constant to 0.
And the result is this - in pic below. At bottom you can see content(text) is not compressed to hide itself. What should I do. I have played with vertical compression resistance. Please don't say set buttons to hidden etc.
// Orange Button Action Clicked
#IBAction func orangeAction(_ sender: Any)
{
redViewHeight.constant = 0
redView.clipsToBounds = true
}
// ClipTo bound : the subview that fits within the bounds of the superview.

View frames don't respect constraints at runtime

I have a UITableViewCell with three labels on it. Two along the top and one that holds some content along the bottom. I constrain the two top labels so that they are 8pts from the top of the view. The left label is 8pts from the leading edge and the right label is 8pts from the trailing edge. I then set the left label to be 12pts minimum from the right label.
Title constraints
Date constraints
It looks fine in Xcode when I evaluate it. I can add a really long title (left topmost label) and it truncates the text correctly, giving me my 12pt margin to the date label.
When I run the app at runtime, the constraints don't seem to be applied. The title label is the full width of the device with the date label no where to be seen.
It does this on the iPhone 5, 5s, 6 and 6 Plus simulators. What am I doing wrong? In my viewDidLoad() method, I am loading the nib containing the UITableViewCell and then registering it. I also add a button to the UINavigationController.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem()
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = controllers[controllers.count-1].topViewController as? DetailViewController
}
// Register our additional nibs
let nibName = UINib(nibName: "StandardNoteCell", bundle: nil)
tableView.registerNib(nibName, forCellReuseIdentifier: identifier)
self.newNoteButton = self.createNewRoundButton()
self.navigationController?.view.addSubview(self.newNoteButton)
}
Does anything stand out as being incorrect?
The problem is that you have two text labels with ambiguous information about how to resolve the case in which they can't both fully fit in the horizontal space available.
To resolve, set the date label's compression resistance to "1000 (Required)". This way the date will always be visible no matter what, and the other views (the title label, in this case) will work around that by shrinking.
As an experiment, try setting both labels' compression resistance to 1000. This will be impossible to achieve, and you'll see errors in your console. So use required constraints sparingly - you want your constraints to be as flexible as possible.
XCode does not know the size of UILabel, you can see your constrains are all red in the horizontal axis.
There is two solutions for your problem
Set the width of the labels
Set the hugging/Compression of the label
If you set the width the label won't resize depending on the size of the screen.
If you set the hugging it will expand/contract depending on the priority of each UITextField
Content Hugging: Don't want to grow
Content Compression Resistance: Don't want to shrink
Just a wild guess but is your date label actually being set with any text? Because if not, it would explain the behaviour you're observing.

Resources