How can I arrange these three UILabels using auto layout programmatically? - ios

I have a UIView that has three UILabels on it. I have a title, subtitle, and subtitleDescription, all UILabel properties. I want my title on the top left, the subtitle below the title no gap, and the subtitleDescription to go to the right of the subtitle no gap, aligning the baseline with the subtitle baseline. I want elipses if the view, or views in the case of the subtitle/subtitleDescription. I Would like to use auto layout programmatically.
Similar to this:
_________________________________
|[title] |
|[subtitle][subtitleDescription] |
|________________________________|
I want the labels to go to the upper left hand side rather than centering. In my code right now, it is all centered and all the labels are on top of each other.
I just call sizeToFit on all the UILabel's, other than that I don't adjust the frame at all. Of course this code is after I alloc and init the labels and set the text. Here is my code:
- (void)setup
{
[self.title sizeToFit];
[self.subtitle sizeToFit];
[self.subtitleDescription sizeToFit];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[title]-(>=0)-|"
options:kNilOptions
metrics:nil
views:#{ #"title" : self.title }]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[subtitle]-5-[subdesc]-(>=0)-|"
options:NSLayoutFormatDirectionLeftToRight
metrics:nil
views:#{ #"subtitle" : self.subtitle,
#"subdesc" : self.subtitleDescription }]];
// compR > compR
[self setContentCompressionResistancePriority:900 forAxis:UILayoutConstraintAxisHorizontal];
[self setContentCompressionResistancePriority:500 forAxis:UILayoutConstraintAxisHorizontal];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[title]-5-[subtitle]|"
options:kNilOptions
metrics:nil
views:#{ #"title" : self.title,
#"subtitle" : self.subtitle }]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self.subtitleDescription
attribute:NSLayoutAttributeBaseline
relatedBy:NSLayoutRelationEqual
toItem:self.subtitle
attribute:NSLayoutAttributeBaseline
multiplier:1.0
constant:0]];
}
Thank you!!!
Here is a screen shot of the view:
Update
As jrturton pointed out to me, it looks like all my constraints are being broken from the constraints exceptions. I am looking to figure out why they are broken. The message given is "Unable to simultaneously satisfy constraints".

First of all, make sure your labels are autolayout-enabled by setting translatesAutoResizingMaskIntoConstraints = NO.
You don't need any of the sizeToFit calls. At the point of adding constraints, they are meaningless. The labels will use their intrinsic size at the point of layout.
To prevent centring, simply don't pin to both sides of the superview. So instead of this:
"|[title]-(>=0)-|"
Do this:
"|[title]"
Or indeed this:
"|[title]|"
And set left alignment on the label.
For multiple labels in a line, you'd want this:
"|[subtitle]-5-[subdesc]|"
Passing NSLayoutFormatAlignAllBaseline in the options. You can OR (|) the options together if required. Again, you don't need the inequality spacing. A left aligned label will take as much space as it needs to. You may want to set compression resistance / hugging priorities on the two labels so you have rules on which one is truncated if there isn't enough room to display both values.
You're setting content compression resistance on the view itself, this is meaningless if the view itself is not also subject to auto layout. You're also setting it twice to two different values, I'm not sure what you are hoping to achieve with that.
Your current vertical constraints will, if the superview has a fixed size, cause one or other of the labels to be stretched to fill the remaining size, which will centre the text vertically, that's what labels do when they are too tall.
You can overcome this by not pinning to the bottom:
"V:[title]-5-[subtitle]"
I've written extensively about VFL and auto layout here, with links to other autolayout-related articles.

Related

Shrink width of UITextField based on growth of another

I am trying to modify the width of a UITextField based on the increasing width of another. Here's an example of my layout:
On the left box (which contains int dialog codes once selected) I want to set this to margin 0 to start with and extend the right UITextField right across. When the left box contains data I want to adjust the right hand text field's border relative to that.
I know how to do this easily in Android but I am not having much luck in Xcode. Can anyone point me in the right direction?
The basic idea is
observe the text changed event
recalculate the width
update the constant property of the width constraint
Here is a simple demo and more edge cases need to be considered.
- (void)viewDidLoad {
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(100, 150, 175, 100)];
container.backgroundColor = [UIColor grayColor];
[self.view addSubview:container];
//initialize text fields
UITextField *leftInput = [[UITextField alloc] initWithFrame:CGRectZero];
leftInput.translatesAutoresizingMaskIntoConstraints = NO;
[leftInput addTarget:self action:#selector(textDidChange:) forControlEvents:UIControlEventEditingChanged];
[container addSubview:leftInput];
UITextField *rightInput = [[UITextField alloc] initWithFrame:CGRectZero];
rightInput.translatesAutoresizingMaskIntoConstraints = NO;
[container addSubview:rightInput];
//setup constraints
NSDictionary *views = NSDictionaryOfVariableBindings(container, leftInput, rightInput);
NSMutableArray *constraints = [NSMutableArray array];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftInput]-[rightInput]-|" options:0 metrics:nil views:views]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftInput]-|" options:0 metrics:nil views:views]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightInput]-|" options:0 metrics:nil views:views]];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:leftInput attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:minWidth];
[constraints addObject:widthConstraint];
self.widthConstraint = widthConstraint;
[NSLayoutConstraint activateConstraints:constraints];
}
- (void)textDidChange:(UITextField *)textInput {
NSDictionary *attributes = #{NSFontAttributeName: textInput.font};
CGFloat width = [textInput.text sizeWithAttributes:attributes].width;
self.widthConstraint.constant = MAX(minWidth, width);
}
Here is an alternative without using constants. The idea is to set up the autolayout in such a way that it recalculates width of the left text field for you so you simply update the layout when text changes.
First, set hugging priority on left text field to be higher than right text field. Make sure you have these horizontal constraints installed:
Left margin of left text field to container (or other item, simply fix the left margin)
Right margin of left text field should have a fixed horizontal spacing to left margin of right text field (this is the gap between the text fields, standard is 8px but of course use what you want)
Right margin of right text field to the right side of the container or whatever you use to anchor the right side to
This forces the two text fields to occupy some given space between the left and right margins, and autolayout must decide which text field gets priority. By setting hugging priority higher for left text field you state that you prefer the right text field to expand if there is space available, therefore if both fields are empty, the right one will take up the full space. This is why you need to set up greater than or smaller than constraints to limit how small/large your text fields can get.
For now, let's assume we start with both text fields empty. At this point it would be smart to provide a minimum width for the left text field because otherwise it will have a width of 0 and so it won't be possible to enter any text. Minimum size of about 50 should be reasonable to allow for text input, but try and see what works for you.
Remember to capture the text editing changed property on the text field, so assuming you have the text field in a variable named leftTextField, call
[leftTextField addTarget:self action:#selector(leftTextChanged) forControlEvents:UIControlEventEditingChanged];
somewhere in your viewDidLoad or other initialisation method.
Now all that remains is to ask autolayout to recalculate the intrinsic size of the views and update itself. This is done by calling layoutIfNeeded, so your method would look like
- (void)leftTextChanged {
[self.view layoutIfNeeded];
}
Hope this helps.

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.

Flexible width constraint

I got stuck with resolving constraints, perhaps someone can help:
Here is the case:
I need myView to have flexible width according to superview width.
If superview has more width than 500 -> myView should have 500.
If superview has less width then 500 -> myView should take all superView width.
[NSLayoutConstraint constraintsWithVisualFormat:#"[myView(==500)]" options:0 metrics:nil views:#{
#"myView" : self.myView,
#"superview" : superview
}];
// If I write myView(<=500), obviously width will be zero.
// I can not add something like this:
// #[tableView(<=superview)]", as width can be less, can be more
So I got stuck here, any thoughts?
Thanks in advance!
Visual Format Language can be kind wonky even by Auto Layout standards, especially when setting priority. The problem is actually conceptual and can be done in IB or with VFL or by the class methods. Anyway, the issue:
When you want some behavior to change in a set way when some condition is met or not met, try switching to priority instead of inequality. In this case, think of the margins: You want to the horizontal margin to be 0 when the total width is under 500, but adding space when more than 500. Constraints you will need:
Required priority 1000: Set myView to less than or equal to 500.
Priority 999: Set myView attached to superview with leading/trailing space=0.
If the space is less than 500, both conditions can be met. If the space is more than 500, than it will start breaking constraints, starting with the lowest priority.
Note that after breaking the margin constraints it won't know where to set the horizontal x placement of myView, so you will need another constraint. Centering in superview will conflict the least, since it can place it horizontally with or without margin constraints broken. There is a way to get it to add space on one side only, but it's a complicated dance-of-the-breaking-constriants leading to messy required programmatic intervention; I think centered-horizontally gets your intended behavior.
You need more than one constraint and the use of priorities. One constraint for myView to equal the superview width as you already have and another for the myView to be NSLessThanOrEqual to 500 as suggested above. Then set the priority of the latter to be greater than the equal width constraint. I think that should be enough for the width. Maybe you'll need a 3rd constraint with lower priority to set the width of myView to 500 if it still shrinks to 0 to avoid ambiguity.
Here's an edit to my answer with some code to illustrate better:
Using visual layout as below allows for the more readable layout (once you're used to the syntax)
NSDictionary *views = #{#"myView" : _myView, #"superView": self.view};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[myView(40)]" options:0
metrics:nil
views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(3#999,>=3)-[myView(<=500)]-(3#999,>=3)-|" options:0
metrics:nil
views:views]];
If put a padding of 3 just to illustrate the shrinking to the edges.
If you wanting to center myView in the superview visual layout will be lacking and you'll need to add a long form constraint like this:
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.myView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
But all of this could done in Storyboards without any code based on the same logic. If not using Storyboards I try to use the visual layout as much as possible and usually only the long form for centering views or when dynamically laying views where the structure is only known at runtime.
As per your question you need to set constraint to myView. Here's what you need to do:
UIView *myView = [UIView new];
myView.translatesAutoresizingMaskIntoConstraints = NO;
myView.backgroundColor = [UIColor blueColor];
[self.view addSubview:myView];
NSDictionary *viewDictionary = #{#"myView":myView};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[myView(200)]" options:0 metrics:0 views:viewDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[myView(<=500)]" options:0 metrics:0 views:viewDictionary]];
NSLayoutConstraint *tailing = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:myView attribute:NSLayoutAttributeTrailingMargin multiplier:1 constant:75];
tailing.priority = 999;
[self.view addConstraint:tailing];
Result:
Hope this might help in solving your problem.
You can visit this link for setting auto layout constraints programmatically for advance help.
ref: http://technet.weblineindia.com/mobile/ui-design-of-ios-apps-with-autolayout-using-constraints-programmatically/
ref: https://codehappily.wordpress.com/2013/10/09/ios-how-to-programmatically-add-auto-layout-constraints-for-a-view-that-will-fit-its-superview/

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)
)

iOS Autolayout: Align left edge of a UILabel with horizontal center in container?

So I'm trying to have a customized UITableViewCell that looks like the following:
And I'm trying to get the Detail labels to be left aligned and set the content hugging on the title labels higher.
So the challenge I have is that I want the left edge of the second column on Title cells to be aligned with the center of the whole view.
Is there a way to do that?
Try using a couple of grouping views, one for each column. Pin the outer edges of each column view to the edges of the cell content view, then add an equal-widths constraint to the column views.
Once you have your containing column views in place, you can layout the labels inside each.
Add equal widths constraint, add leading space constraint to left view, and trailing space to the right, and horizontal spacing between them.
Sure, you could put them in a UIView and that views left edge would be equal to CenterX and thus they would be aligned in the middle. Or you could just align the top title cell to CenterX and have the leading edge of each title match that. There are numerous ways you can achieve what your asking here.
NSLayoutContraint *constraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0f constant:0.0f];
[self.view addConstraint:constraint];
NSArray *constraints [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[label1]-(5)-[label2]-(5)-[label3]|"
options:NSLayoutFormatAlignAllLeading
metrics:nil
views:#{ #"label1" : label1, #"label2" : label2, #"label3" : label3 }];
[self.view addConstraints:constraints];
Above is just samples of how you could do it in the code if you wished too, self.view is a view but if you wanted the content view of the UITableViewCell you could just switch it out to that.
I couldn't get the suggestions above working so I did this another way.
InnerView1 50% width of ContentView & pinned to left
InnerView2 50% width of ContentView & pinned to right
Simple.

Resources