Changing Constraints Programmatically - ios

I have a user form where there are a bunch of Label and its corresponding text field or drop down or checkbox group arranged vertically on Storyboard. Something like this:
Label 1
Text Field 1
Label 2
Check Box Group
Label 3
Drop Down Field
Label 4
Text Field 4
Under normal condition, constraint set up between Labels is 60 vertically.
The problem is since Check Box Group is dynamically populated from the data retrieved from server, I don't know how many checkboxes will be there between Label 2 and Label 3 at StoryBoard design time. So there comes a need to programmatically change the constraint between Label 2 and Label 3.
Currently, what I did was set constraint 60 between them but check "Remove at build time" and programmatically set the constraint depending on how many checkboxes are there between there. Something like this:
[_superView addConstraint:[NSLayoutConstraint constraintWithItem:_Label3 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_Label2 attribute:NSLayoutAttributeBottom multiplier:1 constant:100]];
But for some reasons , this is not working as expected. Although Label3 is correctly placed after the last checkbox, I could only see half of the Label4 which has height constraint. I tried:
[superView updateConstraints];
[superView layoutIfNeeded];
But to no avail.

You can add vertical spacing constraint in storyboard, and add an nslayoutconstraint iboutlet var in view controller and connect the same to the constraint in storyboard.
Now the constraint.constant value can be changed programmatically to whatever value you want to instead of adding it.

Personally, I'd be inclined to not have any constraints between "Label 1", "Label 2", etc. I would instead create a view with container views that will contain the various fields between the labels:
I'd define vertical constraints like so (I'm assuming you'd do this in storyboard, like I did above, but I'm showing example VFL as that's a concise and precise way of describing a whole set of vertical constraints):
V:|-[label1]-[group1(0#250)]-[label2]-[group2(0#250)]-[label3]-[group3(0#250)]-[label4]-[group4(0#250)]-|"
Note, in my snapshot, I gave those container "group" views a non-zero height and a gray background, so you can see them, but hopefully this illustrates the idea. Have placeholder views that will contain the controls underneath the various labels and give that a zero height but a low (250) priority (or, like you did, have it omit them at run time).
Then you can programmatically add whatever controls you want as subviews of their respective group views as set their constraints accordingly, e.g.
V:|-[MoView]-[LarryView]-[CurlyView]-|
And because those have a higher prior than the zero height constraints of the group boxes, the resulting view will be resized accordingly, e.g.:
You can probably accomplish something very similar using UIStackView (if targeting iOS 9 and later), but if using simple constraints, this is how I'd tackle it.
That way, you get out of the business of coding magical spaces between "Label 1" and "Label 2", which will become invalid if, for example, you change fonts at some future date (or better, support the use of the user-provided font sizes specified within the Settings app).

Related

How to add views in an existing view made in Interface Builder with Auto Layout?

I have a created the following auto layout in Interface Builder:
As you can see I didn't give any fix size to the buttons. I would like to add two button programmatically to get to this result:
Adding the constraints programmatically I know how to do that, at least I know the syntax.
My problem is when to create those buttons?
I create the width constraint based on the width of the button 4. If I do it in viewDidLoad (if I'm not wrong), the auto layout hasn't been set yet so the width (and height) will be wrong.
I thought to do it in viewDidLayoutSubviews but as it's called multiple times when loading the viewController, I get multiple buttons stacked on each other and when I go to landscape more buttons are added..
When should I create those button to have the right sizes?
Auto layout is about rules that hold at all times, not (primarily) about frame sizes at any one moment.
You should not care about getting the frame of button 4 when you set up the constraints for buttons 5 and 6. The constraint that you add for buttons 5 and 6 should refer to button 4's width attribute, not its current width in points. That is, you could create a constraint like this:
NSLayoutConstraint* constraint = [NSLayoutConstraint constraintWithItem:button5 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:button4 attribute:NSLayoutAttributeWidth multiplier:1 constant:0];
constraint.active = YES; // OR: [button5.superview addConstraint:constraint]
That's a constraint that will keep button 5's width the same as button 4's width, even as button 4's width changes. You would do the same for height, and for button 6. Etc.
Put another way, the constraints you create at runtime should be similar to those you would create in IB if you were doing this at design time. It doesn't look to me like you've created explicit, fixed height and width constraints on button 4. You've created relative constraints relating its height and width to other views.
One thing you will have to do: since buttons 2 and 4 have trailing space constraints to the container (or its margins), you will need to remove those constraints when you add buttons 5 and 6. Buttons 2 and 4 would have to have trailing constraints to buttons 5 and 6, respectively, and buttons 5 and 6 would have to have trailing constraints to the container. Actually, you should simplify by getting rid of button 4's trailing constraint to the container and replacing it with a trailing alignment constraint to button 2. Likewise, button 6's trailing edge should be aligned with button 5's, not spaced from the superview's. That way, you only have to remove one constraint (button 2's trailing to superview) and add one (button 5's trailing to superview).
You can create the constraints programmatically in viewDidLoad. If you made an IBOutlet for the buttons, then you can access them and get the size like so:
self.myButton.frame.size.height;
You can use the Autolayout Constraints tool, to make this process easier.

Setting constraints in a UITableView cell, keeping UIButtons appropriately spaced

this is something I have a problem with. I have done a bunch of tutorials on constraints in the interface builder and I understand pinning. My app below uses a UITableView and in the UITableView cells there are 4 UIButtons and 4 UILabels. I want to keep the spacing even like below for larger screen sizes. I guess what I mean is I want the spacing to dynamically increase with the screen size but the size of the images remains the same. If I try pinning the left and right UIButtons to their respective edges of the container this distance will not dynamically increase and there will be a big gap in the centre. How can I set it up so the layout is the same that for the smaller screen size?
You can add some transparent views to the superview, and use them as spacers. I thought this was weird at first, but I noticed that Apple recommends it (it even turns up as a suggestion in one of their amazingly informative NSLayout debug messages)
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:
#"H:|[spacer1][view1]\
[spacer2(==spacer1)][view2]\
[spacer3(==spacer1)][view3]\
[spacer4(==spacer1)][view4]\
[spacer5(==spacer1)]|"
options:NSLayoutFormatAlignAllTop
metrics:0
views:viewsDictionary]];
The trick, as you see, is to set each spacer's width as equal to the first spacer. Then you will get even distribution of your visible views. In you case, you would add each of your button/image combos to a container view (view1 ... view4).
In the storyboard...
The blue views represent the (transparent) spacer views. This is the setting to get them to resizeable equal widths. You should also set the visible button/imageview combos to fixed widths.
Although this example says 'add two constraints' it actually adds four, all marked as 'equal width to view' - but it seems to do the right thing. You will also want to set the space between each view ('spacing to nearest neighbour') to zero.

Fluid vertical layout, dynamic labels and autolayout

I am trying to replicate a fluid vertical layout with autolayout in ios7.
The problem is that i am using a stack of UILabels (and others elements) with AttributedStrings (but this seems to not to interfere with the issue since it also comes with plain text) and the text in the labels is added dynamically, but the frame, or better the constraints don't seems to adapt in a correct way automatically (not specifying a height for the labels) since the label remain of the dimensions given in the xib.
Initial configuration in the Xib, blue are setted equality constraints
Same if i use to set a big height constraint constant with value lower of the compression and hugging priority. Nothing change.
After adding text and resizing constraint value (the text should have
5 rows
I finally tryed recreating the contraint every time the text change either this way or with CoreText or many others methods
[self.view setAutoresizesSubviews:NO];
label.lineBreakMode = NSLineBreakByWordWrapping;
label.numberOfLines = 0;
label.preferredMaxLayoutWidth = label.frame.size.width;
[label removeConstraint:oldConstraint];
NSInteger height = ceil([label.attributedText
boundingRectWithSize:CGSizeMake(label.frame.size.width, 10000)
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
//TODO 20 or 1.5 is an hardoced value. find solution to workaround
//attributes:#{NSFontAttributeName:[UIFont fontWithName:kFontCrimsonText_Roman size:17]}
context:nil].size.height) + 1;
NSLayoutConstraint * newConstraint = [NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0f
constant:height];
[label addConstraint:newConstraint];
This time the dimensions of the labels seems correct but the text inside it is not displayed properly:
Lets say i have a text of 5 rows only the first are shown and the lasts are just cutted despite the fact the the frame seems to be perfectly fitting the required dimensions.
Multiplying the height by 2 (or another constant) seems to fix the problem but obviously now the text is more little than the frame and a lot of blank space remain on top and bottom.
Trying with [label fitToSize] seems to works perfectly, but obviously the constraints bring the frame back right after.
What is the best practice to achieve a vertical fluid layout with dynamic UIlabels and autolayout?
The best way to create a "fluid vertical layout"... UITableView.
Seriously, this looks like a perfect candidate for using a UITableView.
It is useful for more than just displaying lists of data. For instance in the Contacts app in iOS the individual contact (where you can add numbers and emails etc...) uses a UITableView to layout the interface.
It takes all of the bother out of having to calculate heights and positions of labels etc...
Define a custom UITableViewCell with the correct layout of one of your UILabels and then you just have to put the right text into the right row.
UITableView is a really powerful tool for things like this.
I ended up finding this was a bug, due to use of AttributedText in a UILabel with Autolayout turned on, in ios7.
A temporary workaround as sugested in the other question was to append a new-line character to the attributed string and reducing "bottom margins".
Another one is tu use textview.
Bug reported also in:
- Lines missing from tall UILabel when embedding NSTextAttachment
- iOS 7 BUG - NSAttributedString does not appear

Autolayout - Match height, not heights (Unidirectional)

Is it possible to have a unidirectional size match using autolayout and interface builder?
For example, I might have two labels. I don't want label A to be larger than label B, and I want B to have its intrinsic size. But using "match heights/widths" could result in a large amount of text increasing A's size, and therefore B's.
The way to do this would be to have two constraints.
An equal heights constraint between the label and the image view.
A height constraint on the image view.
This will first set the height of the image view with the fixed height constraint and then will set the height of the label from the height of the image view (equal heights).
By doing this the label will not grow with the amount of text it has. Its height is effectively fixed by the image view.
It will not make the image view get any bigger as this would contradict the fixed height.
EDIT FOR NEW QUESTION
OK, for this you would do pretty much the same thing. It might be a bit tricky in interface builder though as I'm never sure which is item1 and item2 in the constraint when done through IB.
You could do it very easily though by adding one line of code...
[theSuperview addConstraint:[NSLayoutConstraint constraintWithItem:labelA
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:labelB
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0.0]];
This is exactly what the interface builder constraint does but I'm not sure if you can tell which way around the item1 and item2 are.
This is your "unidirectional" equal heights attribute though.
EDIT 2
There may or may not be an update some time in the future that might let you see item1 and item2 in interface builder.

With iOS layout contraints is it possible to collapse padding between labels when height is 0?

Consider a view that contains three UILabels with vertical contraints defined as follows:
[self.view
addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[label1]-2-[label2]-2-[label3]"
options:0
metrics:nil
views:views]];
This will product a layout with the labels stacked vertically w/ 2px padding between each label.
In my app, sometimes the text displayed on one or more of the labels is nil (or blank) which means the label frame ends up with height == 0. In this case, I want the 2px padding between the 0 height label to be collapsed.
When all labels have a text value, I want the layout to be:
label1 text
[2px]
label2 text
[2px]
label3 text
When label2 has a nil text value, I want the layout to be:
label1 text
[2px]
label3 text
In the latter case, label 2 is actually still there but has 0 height and its 2px padding has been collapsed.
Question: Is this possible? How would I define constraints to accomplish this?
EDIT
I realize it is possible to implement the padding (edge inset) in a subclass of UILabel and take the padding out of the constraints. I may go that route, just hoping that there is a way to define collapsing this padding in the constraint definition.
You can redefine your constraints when the values you are displaying in your labels changes.
If this is a view subclass with three string properties, in the setter of each property, you can call setNeedsUpdateConstraints.
Then, in your updateConstraints implementation, remove the constraints previously created (which you can store in a property if required, since the VFL method returns an array) and regenerate the appropriate ones.
If you are in a view controller which is handling the layout, there is an analagous method for updating the constraints on the view controller's view: updateViewConstraints.
Yes, there is a way to do this, and you should deal with this by modifying your constraints instead of messing with a UILabel subclass. However, it's kind of tricky to do using VFL as you are currently doing though, because what you need to do is hold a reference to the individual constraints between each label, and VFL returns an array of constraints (so you won't know which is which).
So, assuming you're not using VFL, you'll want to create the constraints between each pair of labels individually and store it in an ivar or property:
NSLayoutConstraint *label1To2Constraint = [NSLayoutConstraint constraintWith...];
NSLayoutConstraint *label2To3Constraint = [NSLayoutConstraint constraintWith...];
If you don't explicitly add constraints to set a fixed height on the labels (the VFL code you're using doesn't do this), then if the label has no text in it, it will shrink to zero height. So now all you need to do is after updating the text for the labels, do some checks on whether the labels have text or not and update the constant property of the constraint between each pair as needed:
BOOL hasLabel1Text = label1.text.length > 0;
BOOL hasLabel2Text = label2.text.length > 0;
BOOL hasLabel3Text = label3.text.length > 0;
label1To2Constraint.constant = (hasLabel1Text && hasLabel2Text) ? 2.0f : 0.0f;
label2To3Constraint.constant = (hasLabel2Text && hasLabel3Text) ? 2.0f : 0.0f;
Changing the constant of existing constraints can (and should) be done without removing and re-adding the constraint. It's very efficient. (Removing and re-adding constraints is much more taxing on the internal Auto Layout engine, and should be avoided as much as possible.)
In order to save you some pain, I'd like to point you towards the UIView+AutoLayout category I've created. I promise it will dramatically improve your experience with Auto Layout using the thinnest layer of third party code possible. You'll see that it's very easy to create the constraints you need for this scenario.
This works well for this kind of problem:
https://github.com/depth42/AutolayoutExtensions

Resources