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 are UIView
s that are intended to either sit side-by-side or top/bottom depending on the device orientation. Within each UIView
, 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 the UIView
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
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.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With