Dynamic UILabel with proportional width and text-driven height - ios

I'm trying to programmatically create a container view with two UILabel subviews which behave as follows:
The container width is pinned to its superview; its height is constrained to fit the labels
The labels are laid out horizontally, with standard spacing between them (8pts)
The left label width is 25% of the width of the container
The right label width fills the available space, minus standard horizontal spacing
Long text should be broken at word boundaries are flow across multiple lines; both labels must grow vertically to accommodate long text
I have defined the labels with numberOfLines = 0 and lineBreakMode = NSLineBreakByWordWrapping.
Note that the size of the container is completely dynamic; its width is determined by its superview, while its height is determined by its subviews (the labels). The size of the labels is also dynamic; their widths are proportional to the container width, and their heights depend on the length of the text.
I've been able to achieve everything above, except for the last item, with the following constraints (pseudo-code). A is the left label, B is the right.
A.top == container.top
B.top == container.top
A.leading = container.leading
A.trailing == B.leading - 8
B.trailing == container.trailing
A == .25 * container.width
container.height >= A.height
container.height >= B.height
The last 2 constraints are intended to stretch the container to fit the taller of the labels, but the layout engine seems to ignore the fact that the labels may be multiline. That is, I always get a single line displayed, no matter the length of the text.
So what constraints do I need to add/modify/delete in order to achieve the full set of behaviors described above?

To make your labels automatically resize height you need to do following:
Set layout constrains for label (That what you actually have done)
Set height constraint with low priority. It should be lover than ContentCompressionResistancePriority
Set numberOfLines = 0
Set ContentHuggingPriority higher than label's hight priority
Set preferredMaxLayoutWidth for label. That value is used by label to calculate its height
For example:
self.descriptionLabel = [[[UILabel alloc] init] autorelease];
self.descriptionLabel.numberOfLines = 0;
self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.descriptionLabel.preferredMaxLayoutWidth = 200;
[self.descriptionLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[self.descriptionLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[self.descriptionLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:self.descriptionLabel];
NSArray* constrs = [NSLayoutConstraint constraintsWithVisualFormat:#"|-8-[descriptionLabel_]-8-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(descriptionLabel_)];
[self addConstraints:constrs];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-8-[descriptionLabel_]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(descriptionLabel_)]];
[self.descriptionLabel addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[descriptionLabel_(220#300)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(descriptionLabel_)]];

Set the priority of Height Constraints for the labels to low value and try setting the constraints in code.

Make sure you set
horizontal and vertical content compression resistance priority. If you do not want label to truncate its content then set it to 1000.i.e. required.
Content hugging priority. Look at this answer to understand how it works.

Related

Minimum cell height - Autolayout

I have some Autolayout Cells - but would like that they have a minimum height, if the second Label does not contain any text (for example in an form)
I thought the easiest way should be to create an additional Constraint with Priority 999 to setup an minimum height, like in that pic:
But then, the label ("Mayer Thomas") is not self-sizing anymore.
Whats the best way to solve such things? I could create 2 layouts, but in my opinion that should be not a good solution.
Set the height constraint of the optional label as greater than or equal to whatever height you want. Also, set the compression resistance and content hugging of the label to required. Your label should now consume extra height if there is more content, or just take the minimum size that was set.
[label addConstraint:[NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0 constant:15/*the min height you need*/]];
[label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[label setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
For that You need to use EqualHeights and change the multiplier accordingly they will self-Size themselves .
To do that just CTRL+Drag the label onto the cell gives equal heights and change the multiplier it will be 1 change it to whatever value you want. Eg: - 0.5 means it will take half the height of the cell.

UILabel dynamic height

I know a lot of questions have been asked about it but could not figure out.
I have a label that needs its number of lines adusting dynamically. It works with automatic preferred max width, but could not make it work when explicit (I want to support ios7).
The label height does not currently increase
Here is my code:
-(void)viewDidLoad
{
[super viewDidLoad];
_feedBackLabel.numberOfLines = 0;
_feedBackLabel.lineBreakMode = NSLineBreakByWordWrapping;
_feedBackLabel.preferredMaxLayoutWidth = self.view.frame.size.width - 90;
[_feedBackLabel setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[_feedBackLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
}
-(void)setFeedback:(NSString*)response
{
self.feedBackLabel.text = response;
self.feedBackLabel.hidden = NO;
[self.feedBackLabel sizeToFit];
}
I have pinned my label with a leading and a trailing space to its superview with priority required.
The content hugging and compression resistance are both 750 in the vertical axis.
The label has a height constraint with a priority of 500.
Thanks
Guillaume
Remove height constraint from label and run your code
Actually it was working partially. It was only working for long strings; if the string was just a bit too big for my label then it did not work.
What I did is increase my horizontal content hugging priority to 750, i.e. higher than the height priority which is 500 (all other constraints are required), and set the preferredMaxLayoutWidth dynamically, and it works as expected now.
Thanks
Guillaume

Superview not increasing in height based on the subviews constraint

I have a scrollview and a separate UIView where I placed a series of textFields and labels with constraints which fully occupies the top and bottom. I'm trying to adjust the UIView's height based on its subview constraints but it won't. What is happening is that the view keeps its height and force other textfields to collapse or shrink thus breaking the constraints.
Details
Each subview priority values :
compression = 750
hugging = 250
UIView priority values:
compression = 249
hugging = 749 Set to be lower than the rest.
Most of the textfields has aspect ratio constraint. This causes the field to adjust.
Each subview has vertical/top/bottom spacing between each other. The top and bottom elements has top and bottom constraints to the view as well.
What's on my code:
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
/* I had to adjust the UIView's width to fill the entire self.view.*/
if(![contentView isDescendantOfView:detailsScrollView]){
CGRect r = contentView.frame;
r.size.width = self.view.frame.size.width;
contentView.frame = r;
[detailsScrollView addSubview:contentView];
}
}
Screenshots
The view
This is what currently happens. In this instance it forces the email field to shrink. If I place a height value on it, it does not shrink but the layout engine finds another element to break
Edit:
Solved
Maybe I just needed some break to freshen up a bit. I did tried using constraints before but got no luck. However thanks to the suggestion I went back setting the constraints instead of setting the frame on this one and got it finally working.
Solution:
-(void)viewDidLoad{
[detailsScrollView addSubview:contentView];
[contentView setTranslatesAutoresizingMaskIntoConstraints:NO];
[detailsScrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(contentView,detailsScrollView);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil
views:viewsDictionary];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[contentView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:viewsDictionary];
NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[contentView(==detailsScrollView)]-0-|" options:0 metrics:nil views:viewsDictionary];
}
When you use interface builder to deal with the UIScrollView and its child UIView. usually a top, bottom, left and equal width constraints are set between the UIScrollView and its child which is the contentView in your case.
Without those constraints the other option is to set the content size of the UIScrollView. which was the way of using the UIScrollView before introducing constraints.
So, 1. you should add those constraints programmatically.
By using the constraints, the views frame is no longer needed to resize the views.
So, 2. remove frame setting for your content view.
I am not so happy with the way you set the frame in the viewDidLayoutMethod. if I am going to do that here I would take the frame setting out of the if statement.
The code would be as follow with no if statement:
[detailsScrollView addSubview:contentView];
// then set the constraints here after adding the subview.
Put this code anywhere but not inside your viewDidLayoutSubviews method. it will be a bigger problem than setting the frame in there inside if statement.
Note: Originally, if you are going to set frame in the viewDidLayoutSubviews
method. you should do it for all cases. for example for the if case
and the else case. because, next time this method is going to be
called the views will respond to the constraint. and lose its frame.
Another observation: if you want the view to response to its subviews constraint why you need to set the frame for it? right?
After adding the constraint you may need to call the method constraintNeedsUpdate or another related method.

Autolayout Constraints Priority is not Resolving

I have a view which has a UILabel, a UITableView(tblFilters) and a UIView(btnBaseView)(to keep three other UIButtons).Please check the image below: -
I need to expand the tblFilters height to showcase the options for each category but need to have the btnBaseView always visible on the screen. So basically tblFilters height should not increase beyond a limit.
To achieve this i have applied a height constaint to btnBaseView and gave it Required priority. Same way tblFilters has a height constraint but a DefaultHigh priority.
// Height Constraint of btnBaseView. Height Should always be >=116
btnSectionHeightConstraint = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[btnBaseView(>=116)]" options:0 metrics:nil views:#{#"btnBaseView":btnBaseView}];
[[btnSectionHeightConstraint firstObject] setPriority:UILayoutPriorityRequired];
[self addConstraints:btnSectionHeightConstraint];
// TableView Height Constraint. Height value is being changed when user click on "+" button of table section.
tableHeightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:#"V:[tblFilters(>=%f)]",176.0] options:0 metrics:nil views:#{#"tblFilters":tblFilters}];
[[tableHeightConstraints firstObject] setPriority:UILayoutPriorityDefaultLow];
[self addConstraints:tableHeightConstraints];
But this scheme doesn't seems to work as tableView is covering the entire baseView and pushed the btnBaseView out of visible area.
I have tried by keeping DefaultLow priority to tblFilters as well but no effect. When i debug the code after changing the tblFilters height constraint it print correct priority output in console but no effect over the view.
Can someone please help me in identifying the issue why constraint priority is not working as expected or do i have wrong understanding of this concept. Any help is appreciated.
set tblFilters height constraint priority to be
UILayoutPriorityDefaultHigh
set btnBaseView top margin constraint related by tblFilters to
0
set btnBaseView bottom margin constraint related by bottomView
to be >= 0
and then you change height constraint of tblFilters corresponding to the data
tableHeightConstraints.constant = someValue
view.layoutIfNeeded()

Horizontally align buttons in a view using autolayout programatically

I'm trying to align two to three buttons horizontally in a view. For simplicity, I'll show my attempt of aligning two buttons.
This works for buttons that have a short title:
#"H:|-10-[questionButton1(questionButton2)]-5-[questionButton2]-10-|"
But as soon as one of the buttons gets a bit longer title, it breaks like this:
What I ended up doing is calculating width of each button and then if button1 width is greater than half of the view and greater than button2 width, I've used:
#"H:|-10-[questionButton1(==btn1width)]-5-[questionButton2(>=btn2width)]-10-|"
It kind of works but I don't really like the look of my code with this kind of calculations. Just imagine complexity it adds with the third button. Also, there is a problem if both buttons have pretty long title in which case I would have to figure out if I should reduce the font size to make everything fit.
I'm posting this here because I might be missing some magical thing regarding autolayout since I only started using it in code today. Any kind of help would be greatly appreciated.
--- UPDATE (clarification) ---
I want the buttons to split evenly considering the margins (10 on the outside and 5 between buttons). Ideally they should be the same width if the text size would fit their default size (50%:50% for two buttons and 33%:33%:33% for three buttons). In case the button title exceeds that perfect width, the button should extend its width if it is allowed by other buttons (if others can shrink). If there is no extension or shrinking possible, the big button should reduce font size and repeat the procedure (check if other buttons can shrink). Yeah, I know, I'm asking for a lot :)
--- UPDATE ---
#Sikhapol's answer helped me solve it. I've added a few things to reduce font size, add padding and make button titles go into multiple lines if the text doesn't fit:
btn.contentEdgeInsets = UIEdgeInsetsMake(0, 5, 0, 5);
btn.titleLabel.adjustsFontSizeToFitWidth = YES;
btn.titleLabel.numberOfLines = 0;
btn.titleLabel.minimumScaleFactor = 0.7;
End result:
Use Content Compression Resistance Priority!
You can tell auto layout to try to maintain the equal width of the two labels as best as it can. But you tell it that it's more important to let one of them grow bigger to fit the content inside.
To do this, set priority of the equal width constraint to be lower than the content compression resistance priority of the labels (or buttons).
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *label1 = [[UILabel alloc] init];
label1.text = #"this seems";
label1.backgroundColor = [UIColor orangeColor];
label1.translatesAutoresizingMaskIntoConstraints = NO;
UILabel *label2 = [[UILabel alloc] init];
label2.text = #"completely fine";
label2.backgroundColor = [UIColor orangeColor];
label2.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:label1];
[self.view addSubview:label2];
NSDictionary *views = NSDictionaryOfVariableBindings(label1, label2);
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[label1(label2)]-5-[label2]-10-|"
options:NSLayoutFormatAlignAllCenterY
metrics:nil
views:views];
// Find the equal width constraint and set priority to high (750)
for (NSLayoutConstraint *constraint in horizontalConstraints) {
if (constraint.firstAttribute == NSLayoutAttributeWidth) {
constraint.priority = UILayoutPriorityDefaultHigh;
}
}
[self.view addConstraints:horizontalConstraints];
// Set content compression resistant to required (1000)
[label1 setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[label2 setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// The below code is here to add the vertical center constraints. You can ignore it.
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:label1
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:label2
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];
}
So if the content can fit inside those labels:
But if one of them grow longer:
Content Compression Resistance Priority is a way to tell the auto layout that how far you want the component to maintain it's intrinsic size (thus the name compression resistance).
This approach can also be achieved more easily in the IB. The content resistance priority can be set in the Size Inspector tab (cmd + opt + 5).
If you're using Auto Layout, you can simply use a constraint to ensure that your buttons are always aligned, either vertically or horizontally. In order to align them horizontally (ie align their y values to be the same), simply select the two buttons by holding command and clicking on them individually:
They will appear in Storyboard with selector indicators around them. Now go to the bottom right corner and choose to align their "Vertical Centers". Aligning their vertical centers will align them horizontally (based on your diagramming).
This ensures that they will always be aligned horizontally.
To fix your problem about the text expansion, one way off the top of my head I can think of to get around that is to create a UIView and then putting a UILabel inside to simulate a button. You would have to link up to the view to some IBOutlet to get when it pressed and link that to the function you want it to perform. But UILabel has attributes you can set in Storyboard shown here with the Attributes Inspector:
If you choose "Minimum Font Size", set that value, then your text will shrink automatically as it fills up the allotted space as seen here:
As the text grows to fill its width, you end up with a constraint ambiguity. There's no telling what will happen! You need to use constraint priorities and inequalities (and perhaps altered compression resistance) to resolve this.
Here's code where I disambiguate between two labels so that one can grow at the expense of the other:
let p = self.lab2.contentCompressionResistancePriorityForAxis(.Horizontal)
self.lab1.setContentCompressionResistancePriority(p+1, forAxis: .Horizontal)
But I also needed to use inequalities to set the widths and spacing originally:
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:[v1(>=20)]-(>=20)-[v2(>=20)]", options: nil, metrics: nil, views: d)
)

Resources