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:
viewWillLayoutSubviewsis 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}}viewDidAppearworks 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)?
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.
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.widthAnchorwithself.widthAnchordepending 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, ortrailingAnchorto 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
topAnchorto be 30% of the superview'sbottomAnchor:Then you need to activate it as usual.