UILabel shifts right when animating superview using auto layout - ios

In the .gif below, I have a view that is the same size as the device screen (with the beige background). That view has a subview (with the purple background), which I've positioned using Auto Layout. It's been set to be the same width as its superview, with a constant value of -18, and its CenterX value set to be the same as its superview, so it's centered with 9pts of space on each side.
Inside of that view, is a UILabel, also positioned using Auto Layout. It's been set to be the same width as its superview, with a constant value of -20, and its CenterX the same as its superview, so it's centered with 10pts of space on each side.
When the user taps on the purple view, I want to perform a multi-step animation, where the first step involves the purple view expanding to be the same width as its superview.
I'm doing this using the following code:
[UIView animateWithDuration:3.0 animations:^{
[constraint setConstant:0.0];
[view.superview layoutIfNeeded];
}];
The problem (as seen in the first .gif), is that the UILabel immediately shifts right, and then gradually comes back to center as the animation plays out.
Incorrect animation:
In this next .gif, however, the animation performs correctly. I'm using the same code to perform the animation, but instead of making the UILLabel be the same width as its superview (minus the 20pts), I hard-code the width to be a static value, using 0 as the constraint multiplier, and 357 as the constant value.
Correct animation:
This accomplishes the effect that I'm looking to achieve, but I'd rather not hard-code the width of the UILabel (plus I'm very curious why it's acting this way).
Any ideas?

Related

Updating constraints after rotating a UIView

I have a rectangular UIView whose edges are constrained to its superview. I need to rotate this view by 90 degrees, but then update its constraints so it stays correctly constrained to the superview. If I do
self.overlayView.transform = CGAffineTransformMakeRotation(M_PI * 0.5);
The view rotates, but the width and height stay fixed the wrong way round. Do I need to break the constraints and set them again so that the top is constrained to the superview's left, the left is constrained to the superview's bottom and so on?
Autolayout plays with the frame to do its thing. As per the docs, the frame is undefined when the transform is set to something other than the identity.
Based on this, it would appear that this is fixed in iOS8 (and indeed they reference the above docs, noting that as of iOS8 setFrame method gets called on the transformed view).

Making one UIView track the animation of a single constraint on another

I'm trying to learn how to animate using AutoLayout constraints.
I've got two UIViews, as below:
When I tap the "Up" button, I want the result to look like this. I want the red rectangle to stay the same size, but to stay pinned to the top of the gray rectangle and go along for the ride, like this:
Instead, I get this:
Here's my code:
- (IBAction)upButton:(UIButton *)sender
{
self.heightConstraint.constant = 20;
[UIView animateWithDuration:1.0
animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
- (IBAction)downButton:(UIButton *)sender
{
self.heightConstraint.constant = 438;
[UIView animateWithDuration:1.0
animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
The constraint situation looks like this (screenshot--I don't know how to copy the actual list from the Document Outline). The relevant animated constraint (self.heightConstraint) is highlighted:
I confess that I find Auto Layout Constraints sort of like playing Whack-A-Mole. Can someone please help me understand how I should go about this?
When setting constraints you need to think about how the various items relate to each other and keep in mind that the layout engine needs to be able to compute a top,left and width/height for each item. Too many constraints can be just as problematic as insufficient constraints.
In your case you want the size of the red box to be fixed, so I would set constraints on it for width and height. You also want its position relative to the left edge of the screen to be fixed, so set a leading space constraint to the superview. You want its space to the grey box to be fixed, so set a trailing space constraint to the grey box. Finally you want the top of the red box to be the same as the grey box, so select the orange box in IB, Control-drag to the grey box and select "Top" from the pop up.
For the grey box, set constraints for trailing space to the superview and bottom space to the superview (it will already have a leading space constraint to the red box). Finally, either set a constraint for height or a constraint for top space to superview. This is the constraint you will animate, so create an IBOutlet for it and change it in your code as required.
I'd suggest fixing the height of the orange box. If you have a bottom constraint for the orange box remove it.
These are the constraints I would use on the orange box:
Top to the superview / Leading to the superview / Trailing to the grey box / Height fixed / Width fixed
These are the constraints I would use on the grey box:
Top to the superview / Leading to the orange box / Bottom to the superview / Width fixed
Notice the two constraints that I bolded essentially control each view's height.
I'd attach an IBOutlet to the top constraint of each view so that I could modify these values to match the behavior you want.

Hiding a UIView using AutoLayout constraints

From time to time I have a subview that I would like to remove from a layout. Not only should it be hidden, but it should not be considered part of the view's 'flow', so to speak. An example:
I am looking for a strategy to hide the orange view programmatically. The layout of the boxes, and their content, is via autolayout. Two things to note:
the orange box is defining its vertical height based on the content, plus some top/bottom offsets for margins. So, setting the labels' text to nil will only 'shrink' the view down to it's internal margins, it won't have a height of 0.
Similarly, the vertical spacing between the three boxes mean that even if the orange box's height is 0, the gap between red and yellow will be twice as large as required.
A possible solution
My best suggestion is to add a constraint to the orange box, setting it's height to 0. For this to work, I need to use non-required priorities for all of the vertical constraints inside the orange box. At the same time, the container should update the constant for the constraint that separates the boxes. I don't like this approach so much since the orange box class is defining it's internal constraints with it's superview's behavior in mind. Perhaps I could live with it if the orange box view instead exposes a 'collapse' method that adds the 0 height constraint itself.
Is there a better approach?
You can do this by adding an extra constraint between the yellow and red views of a lower priority, and adjusting the priorities in code.
The short dashed constraint (orangeToRedCon is the outlet) has a priority of 999 (you can't change a required priority to a non-required, so that's why it's not 1000). The long dashed constraint (yellowToRedCon) has a priority of 500 and a constant of 20. In code, you can hide the orange view, and swap those priority levels, and that will cause the yellow view to move up to whatever value you've set for the constant value of yellowToRedCon.
-(void)changePriorities {
self.yellowToRedCon.priority = 999;
self.orangeToRedCon.priority = 500;
[UIView animateWithDuration:.5 animations:^{
self.orangeView.alpha = 0;
[self.view layoutIfNeeded];
}];
}
This method doesn't require any changes in the orange view's height.
In iOS 9 you can use UIStackView for this.
There also are polyfills for older versions: TZStackView and OAStackView
What you could do is have the height constraint of the orange view as an outlet (to be able to access it).
then animate the collapse like so:
[UIView animateWithDuration:0.3 animations:^{
orangeHeightConstraint.constant = 0;
[self.view layoutIfNeeded]
}];
The orange view will have to have a top constraint to the red view and a bottom constraint to the yellow view.
Also make sure to check Clip Subviews in IB or [orangeView clipsToBounds] programatically
I would solve this by including all "necessary" spaces of a subview as part of the subview itself. This way,
1. Red View Height = visible red part + bottom space
2. Orange View Height = visible orange part + bottom space
3. Yellow View Height = visible yellow + bottom space
When you set the Orange View Height to 0 by Autolayout, it will automatically shrink the bottom space to 0 as well.

Increase View Height In Scrollview Using Auto Layout

All the views here (except the nav bar) are in a scroll view. All the scrollview's children have pinned heights and vertical spacing set between them. The top label (Thanks for using...) and bottom button (Toggle) are vertically pinned to the scrollview at the top and bottom respectively. The bottom button is also pinned to the bottom layout guide.
I want a flexible height on the red view. The red view is the only one with an inequality constraint. Height >= 64
The flexible height is working in that the height of the red view automatically expands to 152 to fill the extra space on 4'' devices.
However, I want to expand the height even more. In code, I want to expand the height of that red view to, let's say, 300 when someone taps the Toggle button.
- (IBAction)toggle:(id)sender
{
[self.scrollView layoutIfNeeded];
[UIView animateWithDuration:1.0 animations:^{
self.constraint.constant = 300;
[self.scrollView layoutIfNeeded];
}];
}
When I do this, I get an error in the console. "Unable to simultaneously satisfy constraints." Ending with, "Will attempt to recover by breaking constraint " and it breaks the constraint that I just set for the height of 300.
So....how DO I set the height of that red view to something larger like 300? I assumed if I updated it's height contraint that the contentSize of the scrollView would automatically adjust, but that does not seem to be happening.
You are setting the height of the constraint in code correctly. However, your layout needs some tweaking to get this to work properly.
It looks like you're adding the subviews to the scroll view itself. Instead, you need to add a content view to the scroll view, then add subviews to the content view.
For more information about how to use Auto Layout with a UIScrollView, check out Apple's technical note: developer iOS technotes
In your particular case, I would use Apple's so-called "Mixed Approach". In this approach, you set the content view's frame and the scroll view's content size directly. Calculating the height will be a pain. This involves calculating the height of every individual subview plus margins and spacers.

Maintaining subview relative location after rotation

My app is built on a UIScrollview with a UIImageView subview and various other subviews. When I rotate to landscape, I change the contentSize of the scroll view and resize the image view proportionally to take advantage of the increased horizontal width. The means the height increases as well to maintain the proportions.
My question is, in the case of the blue subview shown, what do I need to do to reposition it such that it maintains it relative position after rotation, given that it's superview is no longer the same size? I have experimented with convertRect:toView: and converPoint:toView:, but I can't seem to get it quite right.
Are you using auto layout? If so, in many cases, the judicious use of constraints can keep that subview in the right place and right size, even as you go from landscape to portrait. But you'd have to share more details about what else is on this view for us to be more specific.
If not using auto layout, you generally can set the view's autosizing mask so it moves to the correct location for you. But in the case of a scroll view subview, you might have your view controller can respond to viewWillLayoutSubviews, updating the contentSize of the scroll view and the frame of the subview to move, accordingly:
- (void)viewWillLayoutSubviews
{
// update the contentSize of the scroll view for the width of the root view, but I'm assuming the
// height won't change
self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, self.scrollView.contentSize.height);
// adjust the frame of the subview you want to move so that it is a certain offset from the bottom
// left corner of the scroll view's `contentSize` (in this case, 10 points from bottom, 10 points from right)
self.subviewToMove.frame = CGRectMake(self.scrollView.contentSize.width - self.subviewToMove.frame.size.width - 10,
self.scrollView.contentSize.height - self.subviewToMove.frame.size.height - 10,
self.subviewToMove.frame.size.width, self.subviewToMove.frame.size.height);
}
The specifics vary based upon details of (a) whether you're using autolayout or not; (b) whether you're creating this subview programmatically or not; and (c) what other content you have in your view and whether the change from portrait to landscape and back results in any change in the vertical height of the scroll view.
To reposition a view, you update its frame. The frame property is of type CGRect, which is a combination of size (CGSize) and origin (CGPoint). If size of your blue view doesn't change, then only origin should be updated.
iOS coordinate system starts from top left corner:
For your blue view you calculate it's origin from the bottom right corner, that is
origin = contentSize - blueViewSize - padding
Do this separately for x and y coordinate, make CGRect with updated origin, and update blue view's frame.
UP: This is how you do it manually, but you can (and better should) let UIKit reposition subviews for you automatically -- learn about autoresizing and autolayout in Xcode's Interface Builder, and Developer manuals

Resources