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.
Related
I am trying to figure out how to make my three buttons in a stackview have relative sizing and keep the same multiplier spacing on different devices, e.g. the buttons will be bigger if i am on an ipad compared to an iphone. Secondly the spacing between left and right edges of buttons will be bigger on an ipad compared to that of an iphone device. So far I currently have three buttons in a stackview. I have added horizontally and vertical allignment to my stackview. I have played around with adding equal heights and width and changed the multiplier as well as adding constraints to the buttons however it did not get my desired result.
Here is a screenshot of how i'd like my items to be placed on all devices:
UIStackView has only static spacing. You have two options:
Change spacing inside code with viewWillLayoutSubviews. I prefer it over viewDidLayoutSubviews because changes will follow rotation animation. But if your calculations will depend on other subviews(not just self.view), these frames will not be updated yet. You can change constraint.constant like this too.
#IBOutlet var stackView: UIStackView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
stackView.spacing = view.bounds.height * 0.05
}
Using only storyboard, you can't add spacing constraint with modifier. But you can add a transparent view which will be your spacer, and apply width/height modifier to it. This can be used both for UIStackView and for plain views.
I prefer adding equal height constraint for all views with same size(like btn1.height = btn2.height, btn1.height = btn3.height), so I can set size with one constraint to all of them(btn1.height = superview.height * 0.1).
Something like this should work for your:
Result:
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()
I am having abit of trouble here trying to make this post description label to grow and shrink based on content size on IOS9. I have a view (I will refer it topView) that I am using as a header for the tableview (So when I scroll up the header disappears). Inside the topView, there are a bunch of stack views. I wish to grow and shrink the post description label in height based on content size. I do know how to do it in simple case where everything is inside the prototype cell (i.e. set estimated row height and set uitableviewautomaticDimensions, set sizetoFit on label and change number of lines to 0). However, this is a different case because the post description label is not really inside the cell, it is in its view before the table view cells.
Note that all items in the view has static height except the postdescription label. Post description label is inside a stack view that is pinned only left and right (So that top and bottom would grow?). Also, the main stack view that contains all elements is pinned towards the four sides with the topview that contains the main stack view also pinned towards the four sides. With this setup, I would expect the topview to grow and shrink based on the content size. However, I do not see that in the output. I dont know if it is the stackview that is holding the label refusing to grow or the top view refusing to grow to allow more space for the stackview for the label. Thanks
UPDATE
Thanks Riadluke, I tried doing something as suggested which is resizing the headerview after calculating the required height. I have placed the following code in viewDidLayoutSubview and it works with an issue
postDescriptionLbl.sizeToFit()
let headerView = commentTableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = TopStackView.frame.size.height + ImageStackView.frame.size.height + postDescriptionLbl.frame.size.height + SpacerStackView.frame.size.height + BottomStackView.frame.size.height
headerView.frame.size.height = height
commentTableView.tableHeaderView = headerView
The issue I now have with this method is that when the view controller appears, I can physically see the postDescription label height grow from the default height in storyboard to the required height. For example, when the VC first appears, I see a line of label with some string being cut off, however after 0.5 second, the headerview and the label grow to the size that I wanted. I know this would be expected because I was calling the manipulation after the viewDidLayout Subview. I was wondering if there is a better way such that I dont see that transition and the view appears to be the right height straight away. Ie. let the view know exactly how high the label is to determine how high the headerview needs to be before appearing on screen?
I'm afraid the view set as tableHeaderView of a UITableView does not get resized automatically. Its height will be fixed to the height it had in IB.
What you have to do is set its size manually and then reassign it as the tableHeaderView so it is displayed in the height you want.
It could take only few lines since you're using autolayout.
You can try this code right after you've set the header view's contents:
//for the target size you have set the width as your tableView's width when it is already displayed on screen.
//note that when it is accesed inside viewDidLoad the tableView's bounds
//may be different to the actual bounds it will be displayed with,
//here I am just using the screen bounds
let targetSize = CGSize(width: UIScreen.mainScreen().bounds.width, height: 10000)
//set the tableHeader's size to its size after its layout constraints are resolved
tableHeader.bounds.size = tableHeader.systemLayoutSizeFittingSize(targetSize)
//reassign it as the tableHeaderView to update the height it will be displayed in
tableView.tableHeaderView = tableHeader
After many many attempts, I could not get anything to work with the original setup. The best I achieved was to resize it after view did appear which is not idea as you see the previous layout.
It is now working with a complete different approach. I have created two prototype cell and have one as "HeaderViewCell" and implemented the following functions
commentTableView.estimatedSectionHeaderHeight = 400
commentTableView.sectionHeaderHeight = UITableViewAutomaticDimension
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
Everything works like a charm after that.
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
}
}
I need a button to set up via autolayout which height will increase if the horizontal space is not enough i.e. after rotation. I have a leading and trailing constraint to the parent view, and in portrait mode there is lack of space, so I was thinking about not to reduce font size, but increase height and set Word Wrap for Line Break. I was experimenting size classes, but in somehow always the wCompact hAny is used, because I am testing in iPhone. So to set different height for button for different size classes had no effect.
Anybody has idea how to set depending button height on the available button width and the content via autolayout constraint?
On iPhone button below needs two lines, not 30px, but appr. 60px.
Why are you using a button? This is not what a button is for. If you have that amount of text to display then use either a UILabel or UITextView. You can then add a gesture recogniser to it to capture the tap.
You can use UILabel and can use UITapGestureRecognizer on it. and set number of lines=0 in nib file and have one action of tap gesture. it will work like a button with almost no code as u want
Because I am suspicious it is not possible with Interface Builder I made it with subclassing an UIButton:
class DynamicHeightButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let size = (self.titleForState(UIControlState.Normal)! as NSString).boundingRectWithSize(CGSizeMake(self.bounds.size.width, CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : UIFont.systemFontOfSize(17)], context: nil)
self.bounds.size.height = size.height + 8
}
}