Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update constraints after superview frame dimensions change

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 UIViews 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 UIViews 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 UIViews 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. enter image description hereenter image description here 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
like image 330
AndroidDev Avatar asked Feb 02 '15 21:02

AndroidDev


1 Answers

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:

  1. 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.
  2. 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.
  3. 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;

  4. 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.

like image 77
Mike Sand Avatar answered Nov 15 '22 20:11

Mike Sand