Search code examples
iosobjective-cuiviewcontrolleruikitnslayoutconstraint

Using modal view frame size in subview layout constraints


Problem: I am having some issues using the correct frame values to simulate viewport percentages when sizing subviews of a modal view-controller. I would rather size subviews using layout constraints instead of explicit frame values.

The frame value is not correctly set for the modal view-controller up to and including viewWillAppear. The two methods that are called in sequence with the desired frame values are viewWillLayoutSubviews and viewDidAppear. Thus far, these appear to be the only "hooks" I can use with access to the correct frame values.

These methods are not easily useable because:

  • viewWillLayoutSubviews is called multiple times with what I am assuming are intermediary frame values. Setting the same constraint(s) multiple times throws an Auto Layout constraint error.

    // Multiple frame values return from viewWillLayoutSubviews
    {{0, 0}, {393, 783}}
    {{0, 0}, {393, 283.66666666666663}}
    
  • viewDidAppear works better in that it is only called once with the final, desired frame value. However, by this point the views are added to the view hierarchy and shown on the screen. Then when setting the layout constraints, the screen brief displays the unconstrained layout before displaying the constrained layout.

Workaround solution: Using viewWillLayoutSubviews and deactivating old constraints before activating new constraints to avoid constraint conflicts. This can be done by either iterating through modal.view.constraints or maintaining a state variable (strong reference) to the original constraints on the first call of viewWillLayoutSubviews. This does not seem to be that clean of a solution.

// Utility function to activate constraints
void setConstraints (UIView* view, NSLayoutAnchor *leading, NSLayoutAnchor *trailing, NSLayoutAnchor *top, NSLayoutAnchor *bottom,
                   CGFloat cLeading, CGFloat cTrailing, CGFloat cTop, CGFloat cBottom, NSArray<NSLayoutConstraint*>* __strong *constraints) {
  NSArray *newConstraints = @[
      [view.leadingAnchor constraintEqualToAnchor:leading constant:cLeading],
      [view.trailingAnchor constraintEqualToAnchor:trailing constant:cTrailing],
      [view.topAnchor constraintEqualToAnchor:top constant:cTop],
      [view.bottomAnchor constraintEqualToAnchor:bottom constant:cBottom]
  ];
  [NSLayoutConstraint deactivateConstraints:*constraints];
  [NSLayoutConstraint activateConstraints:newConstraints];
  *constraints = newConstraints;
}

// UIViewController "hook" with eventually correct frame value
- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  [self.recordingView setConstraints];
}

// Modal view-controller setting auto layout constraints based on layoutMarginsGuide and current modal frame values
- (void) setConstraints {
  UILayoutGuide *guide = self.layoutMarginsGuide;
  CGFloat width = self.frame.size.width;
  CGFloat height = self.frame.size.height;
  setConstraints(self.textField, guide.leadingAnchor, guide.trailingAnchor, guide.topAnchor, guide.topAnchor,
                 0.05*width, -0.05*width, 0.0, 0.2*height, &textFieldConstraints);
  setConstraints(self.progressBar, guide.leadingAnchor, guide.trailingAnchor, self.textField.bottomAnchor, self.textField.bottomAnchor,
                 0.05*width, -0.05*width, 0.08*height, 0.1*height, &progressBarConstraints);
}

Question: Is there a better way to do this that I am not seeing? Or am I not really supposed to be using frame values as viewport width/height percentages (similar to CSS)?


Solution

  • Since your question is asking about a general approach, the following is just an example for setting up the constraints for the text view within its superview.

    [NSLayoutConstraint activateConstraints:@[
        // Set the width as a percentage of the superview's width
        [self.textView.widthAnchor constraintEqualToAnchor:self.layoutMarginsGuide.widthAnchor multiplier:0.9],
        // Align in the center horizontally
        [self.textView.centerXAnchor constraintEqualToAnchor:self.layoutMarginsGuide.centerXAnchor],
        // Set the top
        [self.textView.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
        // Rely on the intrinsic height of the text view or you can optionally set a specific height constraint if desired
    ]];
    

    The first constraint is the one most relevant to your question. This shows how to make the text view be 90% the width of the superview's margin layout width. Adjust as needed. Maybe replace self.layoutMarginsGuide.widthAnchor with self.widthAnchor depending on your needs.

    Set these up once and you're done. No need to update. As the width of the superview changes, so will the width of the text view.


    If you want to set a topAnchor, bottomAnchor, leadingAnchor, or trailingAnchor to be a percentage of a view's height or width, then you need to use the longer form of creating the constraint.

    The following would set the text view's topAnchor to be 30% of the superview's bottomAnchor:

    NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:self.textView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:0.3 constant:0];
    

    Then you need to activate it as usual.