Shrink width of UITextField based on growth of another - ios

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.

Related

NSLayoutConstraint programmatically set view frame

I know there is are similar topics, but i cant figure out how to do simple thing, for example, set view frame programmatically with Auto Layout constraints. Its pretty easy to do with Storyboard, but when i try to learn about NSLayourConstraint i realise that i'm not understand topic.
Consider that we have UIView and 3 buttons. First button at top have 3 constraints (leading and trailing, and to top of a view). Other 2 buttons centered horizontally with that button and have equal widths. its pretty easy layout, i upload screenshot:
I have read about visual format language, but what i cant understand is - how to create constraint, for example, that relay to top (or trailing-leading)? Like following:
Task look pretty simple but still, i did not found a way how to do that programmatically with NSLayoutConstraint. Could you please provide a solution? Thanks.
Here's a solution (should go in -viewDidLoad). There are a a couple of things to note:
Firstly, VFL doesn't allow you to create all possible types of constraint. In particular, centering needs to be done with the +constraintWithItem: class method on NSLayoutConstraint.
Secondly, as noted in the comments, you could just use hardcoded left and right pad values in the horizontal VFL string to achieve the centering, but this might cause problems if you need to support different device sizes.
Thirdly, the call to -setTranslatesAutoresizingMaskIntoConstraints: is critical. Programmatic Autolayout will completely fail to work if you forget this. Also you need ensure all views are added to their superviews before setting up constraints, otherwise any constraint string referencing a superview will cause a crash
NSArray *names = #[#"button1",#"button2",#"button3"];
NSMutableDictionary *views = [[NSMutableDictionary alloc]init];
for(NSUInteger i=0;i<3;i++) {
UIButton *b = [[UIButton alloc]init];
NSString *name = names[i];
[b setTitle:name forState:UIControlStateNormal];
views[name] = b;
[b setBackgroundColor:[UIColor redColor]];
[b setTranslatesAutoresizingMaskIntoConstraints:false];
[self.view addSubview:b];
}
//List of values to be used in the constraints
NSDictionary *metrics = #{
#"buttonWidth":#150,
#"bHeight":#50, //button height
#"topPad":#100,
#"vPad":#20 //vertical padding
};
//Horizontal VFL string (repeated for all rows).
for (NSString *buttonName in views.allKeys) {
NSString *horizontalConstraintString = [NSString stringWithFormat:#"|-(>=0)-[%#(buttonWidth)]-(>=0)-|",buttonName];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:horizontalConstraintString options:0 metrics:metrics views:views];
[self.view addConstraints:horizontalConstraints];
//Can't do centering with VFL - have to use constructor instead. You could also hardcode left and right padding in the VFL string above, but this will make it harder to deal with different screen sizes
NSLayoutConstraint *centerConstraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:views[buttonName] attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
[self.view addConstraint:centerConstraint];
}
//Vertical VFL (vertical spacing of all buttons)
NSString *verticalConstraintString = #"V:|-topPad-[button1(bHeight)]-vPad-[button2(bHeight)]-vPad-[button3(bHeight)]-(>=0)-|";
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalConstraintString options:0 metrics:metrics views:views];
[self.view addConstraints:verticalConstraints];
[self.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)
)

UIView with UILabel subview - responding to AutoLayout changes

I've got a UIView with a UILabel subview that has constraints defined like so:
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-10-[_messageLabel]-50-|" options:0 metrics:nil views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[_messageLabel]-0-|" options:0 metrics:nil views:views];
[self addConstraints:constraints];
In certain instances, the label is not big enough to show all the text, so it is truncated.
When I adjust the size of the UIView within an animation block, the label animates its change in size as appropriate. However, the re-drawing of the text within it kind of 'jumps' -- and fair enough, i wouldn't expect the label to animate a change in the internal drawing of its text.
Anyway, what I'd like to do is fade out this label and perhaps fade in a second to avoid this jerkiness.
My question: Is there a good callback on UIView as to when it will respond to an auto layout change? or is that simply done in layoutSubviews?

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

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.

iOS: How to create a row of controls of same percentage width

it is possible to make in interface builder or with code a row of, for example, three buttons, each with a auto width of 33%, filling entire view horizontal space?
I'm interested in both autolayout / traditional ways.
If using auto layout, you can define the constraints, such that (a) the three subviews are the same width; (b) the first one has a leading edge to the superview; and (c) that the last one has has a training edge to the superview. In visual format language, that means that the layout is
#"H:|[view1][view2(==view1)][view3(==view1)]|"
If doing in in non-autolayout, you just define your frame for the three views such that their widths are precisely 1/3rd the width of the superview and their respective x coordinates are offset accordingly.
So, the code might look like:
UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button1 setTitle:#"1" forState:UIControlStateNormal];
button1.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button1];
UIButton *button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button2 setTitle:#"2" forState:UIControlStateNormal];
button2.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button2];
UIButton *button3 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button3 setTitle:#"3" forState:UIControlStateNormal];
button3.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button3];
NSDictionary *views = NSDictionaryOfVariableBindings(button1, button2, button3);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[button1][button2(==button1)][button3(==button1)]|" options:0 metrics:0 views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[button1]" options:0 metrics:0 views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[button2]" options:0 metrics:0 views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[button3]" options:0 metrics:0 views:views]];
In Interface Builder (IB), you can add the three buttons where the first is locked onto the left edge, the second is locked onto the first button, and the third is locked onto the second button:
You can then select all three buttons and make their width the same:
You can then select the third button and link its trailing edge to the superview:
And you manually adjust the constant for that last constraint to be zero:
And when you do this, all three will be the same size, spanning the view. You can modify the constraints between the buttons if you want to get rid of those gaps, too. I must confess, though, that IB in Xcode 4.6.3 is a little fussy, because it keeps adding constraints that it thinks to make it unambiguous, screwing them up in the process, so some fussy tweaking is sometimes needed. Doing it in code is unambiguous. And I don't think I'm violating the NDA to say that Xcode 5 is more graceful on this sort of stuff.
--
By the way, in iOS 9, this UI may be better rendered using a "stack view". In Xcode 7, select the group of views to be distributed horizontally and then click on the stacked view button, . Then specify the desired distribution for the stack view ("equal centering" or "equal spacing" both work well) and define the constraints to size the stack view properly (e.g. top/leading/trailing constraints).

Resources