Auto Layout: animating changes to NSLayoutConstraint's multiplier - ios

I have this simple view:
- (void)viewDidLoad
{
[super viewDidLoad];
redBox = [[UIView alloc] init];
[redBox setBackgroundColor:[UIColor redColor]];
[redBox setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:redBox];
//H:
widthConstraint = [NSLayoutConstraint constraintWithItem:redBox
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:0.0];
[self.view addConstraint:widthConstraint];
//More constraints...
}
I’d like to animate an increase in redBox's width.
I’ve tried this:
widthConstraint.constant = 100;
[UIView animateWithDuration:1
animations:^{
[self.view layoutIfNeeded];
}
];
this increases the view’s width by 100 and animates it, but I need to increase it by an amount proportional to its superview’s width. In other words, I’d like to set widthConstraint’s multiplier to 0.8.
But NSLayoutConstraint’s multiplier is readonly! Does this mean I have to remove, modify and re-add widthConstraint every time I want to animate a change to its multiplier or is there a better way?

Unfortunately, there is no better way. Removing and re-adding with a different multiplier is your only option. Please file a bug that you would like to see multiplier to be readwrite.

I asked this question before. And we are found only one way, it's remove constraints and added it later with modify multipler. #"It's Apple baby"(©My Boss)
UPD
look at Masonry mb you can replace multipler on plain constrain with constant.

Related

How to add autolayout constraints to a multi-line UILabel inside tableview headerView?

I have searched all day for a solution to this problem. I am trying to have my UILabels autosize themselves based on the length of the text inside a tableViewHeaderView. Normally, with my UILabels inside a UIView, I would set top, leading, and trailing constraints to the UILabel and it would work just like how I wanted. However, I can't get that working inside a tableViewHeaderView. I am able to set top and leading constraints but my trailing constraint doesnt seem to be working. The text goes beyond the width of the screen.
Setting the preferredMaxLayoutWidth property to a number solves the issue but I don't want to have to hard code that.
Correct me if I am wrong, but setting the leading and trailing constraints should be able to give me the width of the view does it not? Then I could set preferredMaxLayoutWidth with that value. But that value is 2403 which is way longer than the width of the screen.
Anyone else experience this?
#import "CustomTableViewHeader.h"
#implementation ReplyHeader{
UILabel *questionLabel;
}
questionLabel = [[UILabel alloc]init];
questionLabel.text = #"SAMPLE TEXT: I had a question about how I can be a better human being using your method. I belive it is an integral part of what it means to be a human so I want to learn more if you are able give more details about it. I also found that what you said about the dogs out there is very cool and would love to learn more about that.";
questionLabel.font = [UIFont fontWithName:#"AvenirNext-Regular" size:17];
questionLabel.lineBreakMode = NSLineBreakByWordWrapping;
questionLabel.numberOfLines = 0;
questionLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:questionLabel];
Constraints
[self addConstraint:[NSLayoutConstraint constraintWithItem:questionLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0f constant:5.0f]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:questionLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:-5.0f]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:questionLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0f constant:10]];
[self addConstraint:[NSLayoutConstraint constraintWithItem:questionLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0f constant:-10]];
update view
- (void)viewDidLayoutSubviews
{
questionLabel.preferredMaxLayoutWidth = questionLabel.frame.size.width;
[self layoutIfNeeded];
}
UITableView's do not get along with AutoLayout. Both header and footer have inherit that problem.
As you might already know, UITableViews are fancy UIScrollViews. If you have ever tried to work with a scroll view, lots of elements and AutoLayout you must know that it does not work well. It lags. That's why UITableView reuse cells. It improves performance and they do not use AutoLayout.
You should try something like
// with this you will get the most compressed size for your view if the constraints properly define it's size
CGFloat height = [footer systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
footer = CGRectMake(CGRectGetMinX(view.frame), CGRectGetMinY(footer.frame), CGRectGetWidth(footer.bounds), height);
// This will tell the layout engine to ignore the view and not try to resize it
view.autoresizingMask = UIViewAutoresizingNone;
self.tableView.tableFooterView = footer;

Animating Constraint Changes, position animates but height jumps without animation

I have a custom UIView. Within that UIView I have a UILabel with a yellow background containing #"8" called labelWhite. I programmatically create 4 constraints for that UILabel: top, left, width, and height.
When I tap the UIView, I change the constant value of the height and animate the layout over 5 seconds. However, the height of the view immediately jumps to the new value, but the y position also changes and jumps to a new value immediately. Then over the 5 second animation, the y position animates back to where it should have stayed all along.
You can see a video of it happening here: http://inadaydevelopment.com/stackoverflow/Constraints0.mov
What it SHOULD do is just remain at y=0 and shrink vertically. What am I doing wrong?
EDIT:
I just discovered that my animations work exactly as intended if my subviews are UIViews, but as UILabels I get the jump-size + animate-position.
What is going on? Is there a way I can get UILabels to animate their size?
This is the code I use to modify and animate the layout:
self.constraintWhiteHeight.constant = secondaryHeight;
[UIView animateWithDuration:5.0 animations:^{
[self layoutIfNeeded];
}];
This is the code I use to create the constraints:
// ------ Top -----
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.labelWhite
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
[self addConstraint:constraint];
// ------ Left -----
self.constraintWhiteLeft = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.labelWhite
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
// ------ Width -----
NSString *constraintString = [NSString stringWithFormat:#"H:[_labelWhite(==%.0f)]", self.bounds.size.width];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintString
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_labelWhite)];
NSLog(#"constraints: %#", constraints);
self.constraintWhiteWidth = [constraints objectAtIndex:0];
[self.labelWhite addConstraints:constraints];
// ------ Height -----
constraintString = [NSString stringWithFormat:#"V:[_labelWhite(==%.0f)]", primaryHeight];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintString
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_labelWhite)];
self.constraintWhiteHeight = [constraints objectAtIndex:0];
[self.labelWhite addConstraints:constraints];
I don't think you're doing anything wrong, this seems to be the way that labels behave when you try to animate their height. I know I've wrestled with this problem in the past, and I can't remember if I've ever found a solution that works by animating the constraints in an animation block. The way I have gotten it to work is to use a timer or CADisplayLink to adjust the height constraint.
-(void)shrinkLabel {
self.constraintWhiteHeight.constant -= 1;
if (self.constraintWhiteHeight.constant >= secondaryHeight)
[self performSelector:#selector(shrinkLabel) withObject:nil afterDelay:.05];
}
After Edit:
Although the timer method works, it doesn't always look smooth, depending on the speed and increments you use. Another way to do this is to use a large UIView (the same size as the yellow label in your movie) with a smaller UILabel inside that has centerX and centerY constraints to the larger view. Then, animate the yellow view as usual with animateWithDuration:animations:
-(void)shrinkLabel {
self.yellowViewHeightCon.constant = secondaryHeight;
[UIView animateWithDuration:5 animations:^{
[self layoutIfNeeded];
}];
}

Auto Layout - make a view's dimensions dependent on another view's

Simple deal: I'd like to make a UIView's width half of its superview's width. Here's my code:
//Display a red rectangle:
UIView *redBox = [[UIView alloc] init];
[redBox setBackgroundColor:[UIColor redColor]];
[redBox setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:redBox];
//Make its height equal to its superview's,
//minus standard spacing on top and bottom:
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[redBox]-|" options:0 metrics:nil views:#{#"redBox": redBox}];
[self.view addConstraints:constraints];
//Make its width half of its superviews's width (the problem):
NSLayoutConstraint *constraint = [NSLayoutConstraint
constraintWithItem:redBox
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:0.0
];
[self.view addConstraint:constraint];
[self.view setBackgroundColor:[UIColor blueColor]];
This is what I get:
If I set multiplier to 1.0, then the view's width is half of its superview. But why is that?
The problem is probably that your red box view is under-constrained. You've given it a width, but nothing to tell it where it should position itself horizontally. Since it should be half the width of the superview, it could choose to position itself on the left half of the superview. Or the right half. Or the middle. You didn't specify, so the framework just gets to pick something.
In this case, it looks like it's choosing to align the center of the red box with the left edge of the superview. This seems like an odd choice, but again, you didn't specify anything. It can pick whatever it wants.
Add another constraint that will position the view horizontally. That should fix the problem.

Resizing UITextView Without Losing Constraints

I'm trying to figure out how to resize UITextView (and everything actually) without losing constraints. Basically, I'm trying to layout a page where most components can have variable sizes (like description). I tried doing it with a simple use case where I have a UITextView and a UIButton underneath. I want to make sure that the position of the button is relative to the bottom of the UITextView.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGRect frame = self.textView.frame;
int height = self.textView.contentSize.height;
frame.size.height = height;
self.textView.frame = frame;
}
What I ended up with is UITextView overlapping with UIButton. After doing a bit of research, it seems that if I replace the frame, all constraints are gone also. I tried copying the constraints over, but of course the pointer is still pointing at the old frame so that didn't help at all.
Is there a good way to solve a very dynamically laid out page? I'm trying to at least use interface builder rather than code everything.
EDITED
I tried updating the constraint as suggested, but that didn't actually resize the UITextView. Did I do it incorrectly? When I get the constant again, it's updated, but the height isn't changed visually. I did simplify my code by adding an IBOutlet for the constraint. Still no luck however.
int height = self.textView.contentSize.height;
self.textViewHeightConstraint.constant = height;
EDITED 2
I figured it out now. I had an extra constraint for the bottom and that was stopping me from actually resizing the UITextView.
The issue is how you've defined your button's top constraint. If it's to the label, when you adjust the label's height constraint, the button will move. For example, if doing it programmatically:
UILabel *label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:label];
label.text = #"Hello world";
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:#"Submit" forState:UIControlStateNormal];
button.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:button];
NSDictionary *views = NSDictionaryOfVariableBindings(label, button);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[label]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[button]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[label]-[button]" options:0 metrics:nil views:views]];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:20];
[label addConstraint:heightConstraint];
Then, if you change the label's height constraint, the button will move:
heightConstraint.constant = 100;
[UIView animateWithDuration:1.0 animations:^{
[self.view layoutIfNeeded];
}];
If you've defined your UI in Interface Builder, select the button and check the top constraint of the button and make sure it's to the label, not the superview:
But, again, if the button's top constraint is to the label, when the label's height constraint changes, the button will move.

UIView not obeying autolayout constraints in UIScrollView

I am adding a UIView to a UIScrollView and constraining it such that it fills the horizontal space, except for some margins. My visual constraint looks like this:
#"|-16-[theLineView]-16-|"
I have made the view one pixel high so it will appear as a line, and placed it between two text labels:
#"V:[someOtherStuff]-[aTextLabel]-[theLineView]-[anotherLabel]"
However, I am finding that the width of the line is only expanding as far as the width of the longest label above/below it.
Why would this be?
P.S I have read this http://developer.apple.com/library/ios/#technotes/tn2154/_index.html
Code
Here is the entirety of the view controller code from a test project that exhibits this issue on the iPad sim.
- (void)viewDidLoad
{
[super viewDidLoad];
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrollView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.scrollView];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[scrollView]|"
options:0
metrics:0
views:#{#"scrollView":self.scrollView}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|"
options:0
metrics:0
views:#{#"scrollView":self.scrollView}]];
self.line1 = [[UIView alloc] init];
self.line2 = [[UIView alloc] init];
self.label1 = [[UILabel alloc] init];
self.label2 = [[UILabel alloc] init];
self.label3 = [[UILabel alloc] init];
for (UILabel *label in #[self.label1, self.label2, self.label3])
{
label.text = #"I am a label and I am long enough that I can be multiline on an iphone but single on ipad";
}
for (UIView *view in #[self.line1, self.line2, self.label1, self.label2, self.label3])
{
view.translatesAutoresizingMaskIntoConstraints = NO;
view.backgroundColor = [UIColor redColor];
[self.scrollView addSubview:view];
}
//horizontal layout - all views/labels should fill the horizontal space expect for margin
for (UIView *view in #[self.line1, self.line2, self.label1, self.label2, self.label3])
{
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-16-[view]-16-|"
options:0
metrics:0
views:#{#"view":view}];
[self.scrollView addConstraints:constraints];
}
//vertical layout - stack em up
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[lab1]-[line1(==1)]-[lab2]-[line2(==1)]-[lab3]-|"
options:0
metrics:0
views:#{#"lab1":self.label1, #"line1":self.line1, #"lab2":self.label2, #"line2":self.line2, #"lab3":self.label3}]];
}
UIScrollView automatically shrinks to fit the views inside it. You need to set the width absolutely somewhere.
Recommended tactic:
Completely fixiate the scrollview inside its parent-view using constraints (leading, trailing, top, bottom).
Create a UIView inside the UIScrollView and put everything you need inside it.
Set the constraints so that the UIView will act as a content-view (this means it is big enough to include all elements). Use intrinsic content-size, shrink-resistance and chaining of elements with constraints a lot. The resulting layout must be well-defined and unique (this means if you were to remove all constraints to the outside, the layout would still work).
Connect the bounds of the UIView with their superview (which is the actual content-view of the UIScrollView, NOT the UIScrollView!).
If you do this in interface-builder (it is possible), you need to re-check your constraints every time you touch something in that scene. And by touch I mean "select" not only "modify".
Found a working solution that should work for your use case, too. See here.
Expanding on number 4 of Patric Schenke's answer; because the content size of scrollView is fluid, pinning an internal view to its edges just doesn't work for determining the width of the view. Your left side pin will work, but both won't. Calculating the width of your view based on the next level container up is the way to go. As long as your self.scrollView is pinned flush to its container(which I call containerView), this line of code will accomplish what you want. Add this line to your for loop for horizontal constraints:
// Pin view's width to match the scrollView container's width
// -32 constant offset for margins
[containerView addConstraint:
[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:-32]];
I found a simple constraint-based way to accomplish this (I haven't tested the extent of the brittleness of this solution):
...#"H:|-16-[view]-16-|"... // (your original constraint)
[self.scrollView addConstraint:
[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]];
This stretches view all the way out to the other side of the view. This is probably not ideal for content that scrolls horizontally, but should work vertically.
I realize this is over a year later, but it's simpler for the single dimensional scrolling use case than patric.schenke's answers (which are good and more robust).

Resources