Update constraints after superview frame dimensions change
Generally it’s a bad idea to mix frames with Auto Layout. (The exception is a view hierarchy that uses constraints containing a view that doesn’t, which then doesn’t use any constraints from that point down [and additional caveats]). One big problem is the constraint system generally won’t get any information from setFrame.
Another rule of thumb is that setFrame and the traditional layout tree is calculated before the constraint system. This may seem counter intuitive with the first part, but remember that 1) in the traditional layout tree the views lay out their subviews and then call layoutSubviews on them, so each one’s superview frame is set before it lays itself out but 2) in the constraint system, it tries to calculate the superview frame from the subviews, bottom-up. But after getting the information bottom up, each subview reporting up info, the layout work is done top-down.
Fixing
Where does that leave you? You’re correct that you need to set this programmatically. There’s no way in IB to indicate you should switch from top-bottom to side-to-side. Here's how you can do that:
- Pick one of the rotation and make sure all constraints are set up the way you want it in Interface builder- for example, each colored view puts 8 points (your spacer view) from superview. The “clear constraints” and “update frames" buttons in the bottom will help you and you’ll want to click it often to make sure it’s in sync.
- Very important that the top-left view only be connected to the superview by the left(leading) and top sides, and the bottom right only connected by the right(trailing) and bottom sides. If you clear the sizes setting the height and width fixed, this will produce a warning. This is normal, and in this case can be solved by setting “equal widths” and”equal heights” and part of step 3 if necessary. (Note the constant must be zero for the values to be truly equal.) In other cases we must put a constraint and mark it “placeholder” to silence the compiler, if we’re sure we'll be filling information but the compiler doesn’t know that.
-
Identify (or create) the two constraints that links the right/bottom view to something to the left and to the top. You might want to use the object browser to the left of IB. Create two outlets in the viewController.h using assistant editor. Will look like:
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomViewToTopConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *rightViewToLeftConstraint;
Implement updateConstraints in the viewController. Here’s where the logic will go:
.
-(void)updateViewConstraints
{
//first remove the constraints
[self.view removeConstraints:@[self.rightViewToLeftConstraint, self.bottomViewToTopConstraint]];
if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) {
//align the tops equal
self.bottomViewToTopConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
//align to the trailing edge by spacer
self.rightViewToLeftConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:self.spacer];
} else { //portrait
//right view atached vertically to the bottom of topLeftView by spacer
self.bottomViewToTopConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:self.spacer];
//bottom view left edge aligned to left edge of top view
self.rightViewToLeftConstraint = [NSLayoutConstraint constraintWithItem:self.bottomRightView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.topLeftView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0];
}
[self.view addConstraints:@[self.rightViewToLeftConstraint, self.bottomViewToTopConstraint]];
[super updateViewConstraints];
}
Since you can’t change constraints after they’re added (except the constant) we have to do this remove-add step. Notice the ones in IB might as well be placeholders, since we’re removing them every time (we could check first). We could modify the constant to some offset value, for example relating to the superview by spacer + topViewHight + spacer. But this mean that when auto layout goes to calculate this view, you’ve made assumptions based on some other information, which could have changed. Swapping out the views and changing what they relate the factors that are meant to change each other connected.
Note that because Auto Layout will use the constraints here when passing information up, first we modify them, then we call super. This is calling the super class private implementation to do the calculations for this view, not the superview of this view in the view hierarchy, although in fact the next step will be further up the tree.
Related videos on Youtube
AndroidDev
Updated on September 15, 2022Comments
-
AndroidDev over 1 year
I have a very simple
UIViewController
that I am using to try to better understand constraints, auto layout, and frames. The view controller has two subviews: both areUIView
s that are intended to either sit side-by-side or top/bottom depending on the device orientation. Within eachUIView
, there exists a single label that should be centered within its superview.When the device is rotated, the
UIView
s update correctly. I am calculating their frame dimensions and origins. However, the labels do not stay centered and they do not respect the constraints defined in the storyboard.Here are screenshots to show the issue. If I comment out the
viewDidLayoutSubviews
method, the labels are perfectly centered (but then theUIView
s are not of the correct size). I realize that I could manually adjust the frame for each of the labels, but I am looking for a way to make them respect their constraints within the newly resized superviews. Here is the code:#import "ViewController.h" @interface ViewController () @property (nonatomic) CGFloat spacer; @end @implementation ViewController @synthesize topLeftView, bottomRightView, topLeftLabel, bottomRightLabel; - (void)viewDidLoad { [super viewDidLoad]; topLeftLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; bottomRightLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.spacer = 8.0f; } - (void)viewDidLayoutSubviews { if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) { [self setupTopLeftForLandscape]; [self setupBottomRightForLandscape]; } else { [self setupTopLeftForPortrait]; [self setupBottomRightForPortrait]; } } - (void) setupTopLeftForPortrait { CGRect frame = topLeftView.frame; frame.origin.x = self.spacer; frame.origin.y = self.spacer; frame.size.width = self.view.frame.size.width - 2*self.spacer; frame.size.height = (self.view.frame.size.height - 3*self.spacer) * 0.5; [topLeftView setFrame:frame]; } - (void) setupBottomRightForPortrait { CGRect frame = bottomRightView.frame; frame.origin.x = self.spacer; frame.origin.y = topLeftView.frame.size.height + 2*self.spacer; frame.size.width = topLeftView.frame.size.width; frame.size.height = topLeftView.frame.size.height; [bottomRightView setFrame:frame]; } - (void) setupTopLeftForLandscape { CGRect frame = topLeftView.frame; frame.origin.x = self.spacer; frame.origin.y = self.spacer; frame.size.width = (self.view.frame.size.width - 3*self.spacer) * 0.5; frame.size.height = self.view.frame.size.height - 2*self.spacer; [topLeftView setFrame:frame]; } - (void) setupBottomRightForLandscape { CGRect frame = bottomRightView.frame; frame.origin.x = self.topLeftView.frame.size.width + 2*self.spacer; frame.origin.y = self.spacer; frame.size.width = topLeftView.frame.size.width; frame.size.height = topLeftView.frame.size.height; [bottomRightView setFrame:frame]; } @end
-
AndroidDev about 9 yearsThanks very much for this. The only significant modification that I made to your suggestion is that I realized that the only constraints that need to be modified were the height and width of each
UIView
. Since they are pinned to the upper left and bottom right corners of the superview, that's all that needs to change to align the views correctly. Thanks again! -
Mike Sand about 9 yearsGlad that could help, problem was well set up, I understand Auto Layout better after answering. Question: I see how just adjusting the height and width of the two colored views would work, but where are the amounts you are adjusting to coming from?
-
AndroidDev about 9 yearsThey are calculated as a percentage of the superview. e.g.
self.superview.frame.size.width - 2 * spacer
-
Mike Sand about 9 yearsThat's what I thought, and in practice probably quite manageable, but still mixing frames and constraints could be problematic, particularly if something in these constraints was meant to affect the superview's frame. In this case not an issue. There are other tricks. Best of luck.
-
kidcoder over 6 years@MikeSand if one creates a
UIViewController
completely programmatically and thus has to create aUIView
inloadView()
and set it as the view controller's view property, can constraints replace the need to ever explicitly set the view's frame? -
Mike Sand over 6 years@sconewolf I don’t think that’s a very good idea, and you’re on your own