Update constraints after superview frame dimensions change

14,550

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.

Share:
14,550

Related videos on Youtube

AndroidDev
Author by

AndroidDev

Updated on September 15, 2022

Comments

  • AndroidDev
    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 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 here enter 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
    
  • AndroidDev
    AndroidDev about 9 years
    Thanks 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
    Mike Sand about 9 years
    Glad 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
    AndroidDev about 9 years
    They are calculated as a percentage of the superview. e.g. self.superview.frame.size.width - 2 * spacer
  • Mike Sand
    Mike Sand about 9 years
    That'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
    kidcoder over 6 years
    @MikeSand if one creates a UIViewController completely programmatically and thus has to create a UIView in loadView() 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
    Mike Sand over 6 years
    @sconewolf I don’t think that’s a very good idea, and you’re on your own