Scroll hesitation with floating view in UIScrollView using auto layout - ios

QUESTION:
I'll simplify the question but keep my original for reference...
I am modifying the priority of existing constraints, but the result only changes the position of one of the UIScrollView subviews. All remaining subviews maintain their original size and position, yet it looks like I am incurring a layout pass on all subviews below the one I am modifying constraints on. So, why is ViewWillLayoutSubviews and UpdateViewConstraints being called on things that haven't changed?
[Original question]
See the details below. What is the cause of the scrolling hesitation seen in the included screencasts and how can I fix it?
BACKGROUND:
I’ve built an accordion style list control that hosts the views of several child UIViewControllers, each paired with a header view to enable the user to toggle visibility of its content view. I’ve created this list control using a UIScrollView with auto layout. I’ve become very familiar with the intricacies of auto layout with a UIScrollView but admit that I am pretty new to auto layout in general. I’ve relied heavily on Apple’s documentation and related blog posts from the community:
Apple's Documentation
Relevant StackOverflow questions
among many, many others.
I’ve implemented this control so that the header views can be floated above other UIScrollView content. Very much like the section views of a grouped UITableView, they will stick to the top of the UIScrollView as the user scrolls down to view more content. Incidentally, I originally built this using a UITableView, but the way it manages visible cells caused scrolling performance issues of its own.
PROBLEM:
I’m having some performance issues when scrolling content. I’ve done some troubleshooting, and I’ve found that when the “floating header” feature is disabled, scrolling performance is pretty good (although there is still some hesitation on expanding/collapsing a section which may have the same cause as my scrolling performance issue). But when this feature is enabled, scrolling hesitates as each header view is floated. I’ve included a screencast of my prototype running on my iPod Touch 5.
Screencast of prototype running on iPod Touch 5
It’s a very minor hesitation, but this prototype has significantly less complex content views. The final project shows hesitation of up to about a second.
DETAILS:
The prototype has been built using Xamarin, but I'm proficient in Objective-C if that's how you want to answer. Here’s how I’ve set up my constraints to support this feature. I’ve done this in a Reload() method that modifies the UIScrollView subviews.
UIView previousContent = null;
for (var sectionIdx = 0; sectionIdx < this.Source.NumberOfSections (this); sectionIdx++) {
var vwHeader = this.Source.GetViewForHeader (this, sectionIdx);
var vwContent = this.Source.GetViewForSection (this, sectionIdx);
this.scrollView.AddSubview (vwHeader);
this.scrollView.AddSubview (vwContent);
this.scrollView.BringSubviewToFront (vwHeader);
var headerHeight = this.Source.GetHeightForHeader (this, sectionIdx);
var isSectionCollapsed = this.Source.GetIsSectionCollapsed (this, sectionIdx);
// This will never change, so set constraint priority to Required (1000)
var headerHeightConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, headerHeight);
headerHeightConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (headerHeightConstraint);
// This constraint is used to handle visibility of a section.
// This is updated in UpdateConstraints.

 var contentZeroHeightConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, 0.0f);
if (isSectionCollapsed)
contentZeroHeightConstraint.Priority = (float)UILayoutPriority.Required - 1.0f;
else
contentZeroHeightConstraint.Priority = (float)UILayoutPriority.DefaultLow;

 this.AddConstraint (contentZeroHeightConstraint);

 // Set initial state of dictionary that keeps track of all inline and floating header constraints
if (!this.inlineConstraints.ContainsKey (sectionIdx))
this.inlineConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ());
this.inlineConstraints [sectionIdx].Clear ();
if (!this.floatConstraints.ContainsKey (sectionIdx))
this.floatConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ());
this.floatConstraints [sectionIdx].Clear ();

 // If this is the first section, pin top edges to the scrollview, not the previous sibling.

 if (previousContent == null) {
// Pin the top edge of the header view to the top edge of the scrollview.
var headerTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, 0.0f);
headerTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerTopToScrollViewTopConstraint);
this.AddConstraint (headerTopToScrollViewTopConstraint);
// Also pin the top edge of the content view to the top edge of the scrollview, with a padding of header height.
// This is done to minimize constraints that need to be modified when a header is floated.

 // May be safely changed to pin to the bottom edge of the header view.
var contentTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, headerHeight);
contentTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentTopToScrollViewTopConstraint);
} else {
// Pin the top edge of the header view to the bottom edge of the previous content view.
var previousContentBottomToHeaderTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwHeader, NSLayoutAttribute.Top, 1.0f, 0.0f);
previousContentBottomToHeaderTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;

 // Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.

 this.inlineConstraints [sectionIdx].Add (previousContentBottomToHeaderTopConstraint);

 this.AddConstraint (previousContentBottomToHeaderTopConstraint);
// Also pin the top edge of the content view to the bottom edge of the previous content view.
// This is done to minimize constraints that need to be modified when a header is floated.
// May be safely changed to pin to the bottom edge of the header view.

 var previousContentBottomToContentTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwContent, NSLayoutAttribute.Top, 1.0f, -headerHeight);

 previousContentBottomToContentTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (previousContentBottomToContentTopConstraint);
}
// If this is the last section, pin the bottom edge of the content view to the bottom edge of the scrollview.
if (sectionIdx == this.Source.NumberOfSections (this) - 1) {
var contentBottomToScrollViewBottomConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Bottom, 1.0f, 0.0f);
contentBottomToScrollViewBottomConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentBottomToScrollViewBottomConstraint);
}
// Pin the leading edge of the header view to the leading edge of the scrollview.
var headerLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f);
headerLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerLeadingToScrollViewLeadingConstraint);
this.AddConstraint (headerLeadingToScrollViewLeadingConstraint);
// Pin the leading edge of the content view to the leading edge of the scrollview.
var contentLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f);
contentLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentLeadingToScrollViewLeadingConstraint);
// Pin the trailing edge of the header view to the trailing edge of the scrollview.
var headerTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
headerTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
// Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.
this.inlineConstraints [sectionIdx].Add (headerTrailingToScrollViewTrailingConstraint);
this.AddConstraint (headerTrailingToScrollViewTrailingConstraint);
// Pin the trailing edge of the content view to the trailing edge of the scrollview.
var contentTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
contentTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh;
this.AddConstraint (contentTrailingToScrollViewTrailingConstraint);
// Add a width constraint to set header width to scrollview width.
var headerWidthConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f);
headerWidthConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (headerWidthConstraint);
// Add a width constraint to set content width to scrollview width.
var contentWidthConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f);
contentWidthConstraint.Priority = (float)UILayoutPriority.Required;
this.AddConstraint (contentWidthConstraint);
// Add a lower priority constraint to pin the leading edge of the header view to the leading edge of the parent of the scrollview.
var floatHeaderLeadingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this, NSLayoutAttribute.Leading, 1.0f, 0.0f);
floatHeaderLeadingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderLeadingEdgeConstraint);
this.AddConstraint (floatHeaderLeadingEdgeConstraint);
// Add a lower priority constraint to pin the top edge of the header view to the top edge of the parent of the scrollview.
var floatHeaderTopEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1.0f, 0.0f);
floatHeaderTopEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderTopEdgeConstraint);
this.AddConstraint (floatHeaderTopEdgeConstraint);
// Add a lower priority constraint to pin the trailing edge of the header view to the trailing edge of the parent of the scrollview.
var floatHeaderTrailingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this, NSLayoutAttribute.Trailing, 1.0f, 0.0f);
floatHeaderTrailingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow;
// Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline.
this.floatConstraints [sectionIdx].Add (floatHeaderTrailingEdgeConstraint);
this.AddConstraint (floatHeaderTrailingEdgeConstraint);
previousContent = vwContent;
}
All content in a UIScrollView needs leading, top, trailing and bottom edge constraints, so that the UIScrollView can determine its ContentSize, so I have done that. As you can see, I’ve added the floating header constraints, even though at execution time no headers should float. I’ve given them a lower priority so that they’re not applied by default. I’ve done the same with a content height constraint for a collapsed section. I’ve done this so that I don’t have to add/remove constraints to float a header or collapse a section, I just need to modify constraint priorities. I don't know if that's good practice but I thought it might help avoid unnecessary layout passes.
I’m keeping track of the constraints that apply to both inline and floating headers. When it’s determined that a header should be floated, I lower the priority of the relevant inline header constraints to DefaultLow and increase the priority of the relevant floating header constraints to DefaultHigh. I do that in an event handler for the UIScrollView’s Scrolled event. I determine which section is occupying the space at ContentOffset and float its header. I’m keeping track of the last index of the header that’s been floated, just to avoid inlining something that doesn’t need to be inlined.
private int lastFloatHeaderIdx = -1;
private void scrolled (object sender, EventArgs e) {
// Restore the code below to see the scroll hesitation from what I think are unnecessary calls to ViewWillLayoutSubviews and UpdateViewConstraints
// How can I achieve this behavior without incurring the unnecessary expense?
if (this.Source != null) {
for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) {
var headerHeight = this.Source.GetHeightForHeader (this, idx);
var vwContent = this.Source.GetViewForSection (this, idx);
var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height));
var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f);
if (sectionFrame.IntersectsWith (scrollContent)) {
this.floatHeader (idx);
 } else if (idx > this.lastFloatHeaderIdx) { // This is an unnecessary optimization. Appears to have no effect.
var inlines = this.inlineConstraints [idx];
if (inlines.Count > 0 && inlines [0].Priority < (float)UILayoutPriority.DefaultHigh) { // This is also an unnecessary optimization. Appears to have no effect.
this.inlineHeader (idx);
}
}
}
}
}
I’ve done some additional troubleshooting by added logging to the ViewWillLayoutSubviews and UpdateViewConstraints of the child UIViewControllers, and I can see that when a header is floated, a layout pass is done on the previous content view and all views below it. I believe this is the cause of the hesitation. I don’t think its a coincidence that the layout pass includes the previous content. To float the header, I have to deprioritize the constraint pinning its top edge to the bottom of the previous content view and increase the priority on the constraint pinning its top edge to the top edge of the UIScrollView.
But since the size and position of the content views inside the UIScrollView don’t change, I don’t think I should be incurring a layout pass on anything. And, I’ve found that sometimes I don’t. For example, if I flick to quickly scroll to the bottom, the headers are floated one after the other as expected, but no layout passes occur — at least not until the scroll velocity slows. I’ve included a screencast of my prototype running in the simulator, with console output.
Screencast of prototype running in the simulator with console output
I’ve also included a link to the source.
Archive of source

While I think you'd probably be better served addressing your mentioned performance problems via UITableView rather than reinventing UITableView, there are definitely some places here that look suspicious. You should first run your code through Instruments to see where the real problems are. Trying to optimize without spending some time profiling is usually a goose-chase.
But still, let's look at some parts of your loop. Loops are often where are problems are.
for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) {
var headerHeight = this.Source.GetHeightForHeader (this, idx);
var vwContent = this.Source.GetViewForSection (this, idx);
var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height));
var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f);
This is calling a lot of functions repeatedly that you shouldn't need to. NumberOfSections should only be called once. GetHeightForHeader had better be very cheap, or else you should cache its results in an array. Similarly GetViewForSection. If that isn't a simple array lookup, you should turn it into one. You're also generating scrollContent for every section, but it's always the same.
Finally, I would give a strong look at floatHeader and inlineHeader. Make sure that these already know their exact values and don't have to calculate a lot of stuff. Your loop should do nothing but find what view has a range of Y coordinates that overlap the current Y coordinate (you don't need a full IntersectsWith, just the Y coordinate), and then adjust either 1 or 2 view's Y coordinate (the current floating view, or the previous floating view and the new one). You shouldn't need anything else going on here.
But step one is to run it through Instruments and see what jumps out.

Related

Programmed Slider Constraint is Not Updating

I have am attempting to learn how to populate a view in my storyboard with sliders and buttons, programmatically. I am trying, currently, to get one programmed slider to adhere to a programmed NSLayoutConstraint
Here is my code:
let centerXConstraint = NSLayoutConstraint(item: self.volumeSliderP, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 10.0)
self.view.addConstraint(centerXConstraint)
I should mention, that when I substitute the first item for a slider which already exists on the view (which was placed via Storyboard, with it's own constraints also placed with IB/Storyboard), it does updated correctly with the above NSLayoutConstraint code. Also, I have been able to update my programmed volumeSliderP with custom code to change it's handle and rotate it to vertical successfully.
What step am I missing to allow this NSLayoutConstraint code to work upon my programmed slider?
Thank you for any help!
When working with constraints in code, you need to do two (maybe three) things, regardless of control type:
Set the translatesAutoresizingMaskIntoConstraints to false.
Failure to do so will set off constraint conflicts, which will appear in the console log. I usually create an extension to UIView for this:
public func turnOffAutoResizing() {
self.translatesAutoresizingMaskIntoConstraints = false
for view in self.subviews as [UIView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
Then in viewDidLoad (after adding my subviews) I simply add a line:
view.turnOffAutoResizing()
Consider if any subviews have intrinsic content size.
As explained in the linked Apple doc, if you have label and a text field, the text field will expand to fit the label without the need for setting widths. A UISlider does not have an intrinsic width but it does have an intrinsic height.
So in your case you need to not only set position, it needs to define the width.
A combination of top and leading will yield enough for the layout engine to know "where" and "height", but not "width". Same would go if you defined "centerX" and something - you didn't list any code - for the Y factor (top, bottom, centerY).
If I'm stating this clearly, you should be able to see that the engine will know enough to say (in frame coordinates) "start the slider at X/Y, height is XX points (it has intrinsic height), but how long should it be?"
I typically set either top, leading, and trailing... or top, centerX, and width. But it varies with the need.

Parent UIView not resizing after constraint.constant on UILabel changed

UPDATE
Now Solved The problem was that when I was updating the bottomConstraint I was setting the Constant to the bottom padding property. Sounds reasonable but of course the Constant should have been set to 0 - BottomPadding. That explains why the bottom of the text was being not visible, it was being constrained beyond its clipping container.
I have a simple UIView custom control called PaddedLabel that wraps (not inherits) a UILabel
The view hierarchy is
PaddedLabel -> UILabel
When the constraints on the UILabel have their constants updated the outer View does not chnage height. It is as if the outer UIView is seeing only the Height of the Label as the Height it needs rather than the Height of the Label plus constants. This is how it looks
In UpdateConstraints I add some constraints and if there is a Text value I set the Constant on the Constraint to the value I want for padding else I set the Constant to 0.
public override void UpdateConstraints()
{
base.UpdateConstraints();
if (this.constraintsApplied == false)
{
this.leftConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1.0f, this.LeftPadding);
this.AddConstraint(this.leftConstraint);
this.rightConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Right, NSLayoutRelation.Equal, this, NSLayoutAttribute.Right, 1.0f, 0 - this.RightPadding);
this.AddConstraint(this.rightConstraint);
this.topConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1.0f, this.TopPadding);
this.AddConstraint(this.topConstraint);
this.bottomConstraint =
NSLayoutConstraint.Create(this.NestedLabel, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this, NSLayoutAttribute.Bottom, 1.0f, 0 - this.BottomPadding);
this.AddConstraint(this.bottomConstraint);
this.constraintsApplied = true;
}
if (this.Text.HasValue())
{
this.topConstraint.Constant = this.TopPadding;
// The following code was the problem.
// It should have been 0 - this.BottomPadding Now corrected
// this.bottomConstraint.Constant = this.BottomPadding;</del>
this.bottomConstraint.Constant = 0 - this.BottomPadding;
}
else
{
this.topConstraint.Constant = 0;
this.bottomConstraint.Constant = 0;
}
}
When the Text property is set I set the Text property on the inner UILabel and call SetNeedsUpdateConstraints
public string Text
{
get
{
return this.text;
}
set
{
if (this.text == value)
{
return;
}
this.text = value;
this.nestedLabel.Text = value;
this.SetNeedsUpdateConstraints();
}
}
If you want the PaddedLabel view to expand and fit around the inside UILabel, change the bottom constraint. You want to tie the bottom of PaddedLabel to the bottom of the UILabel, so as the UILabel grows it makes PaddedLabel expand! The way it is now, you're telling the UILabel to squish itself inside of the PaddedLabel view.
Reverse the bottomConstraint and you should be set.

How to set a constraint using percentage?

Just a very direct question, but we had spent many hours trying to find a working solution but faild.
In Xcode, storyboard, how to set a constraint so one view can be located 30% of total window height from the top of the superview? And we need it to be that way for ALL supported iOS devices of all orientations.
Please see my illustration attached.
I demonstrate this below - you just have to change the value of multiplier.
Update
Sorry, I have misunderstood your problem.
You'll need to add the constraints from code like so (the xConstraint is totally arbitrary, but you must need to define x, y positions, width and height, for an unambiguous layout):
#IBOutlet weak var imageView: UIImageView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let yConstraint = NSLayoutConstraint(item: imageView, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: view.bounds.height / 3)
let xConstraint = NSLayoutConstraint(item: imageView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1, constant: 30)
NSLayoutConstraint.activateConstraints([yConstraint, xConstraint])
}
This way, the equation will be:
imageView.top = 1 * view.top + (view.width / 3)
Original answer
Auto Layout uses the following equation for constraints:
aView.property = Multiplier * bView.property + Constant
Based on this, you can simply add an equal width/height constraint, then add a multiplier:
So the equation will be:
view.height = 0.3 * superView.height + 0
You should calculate it.
1. Calculate how many percents are from top to center ImageView
2. Set Vertical center to ImageView
3. Configure multiplier in Vertical center constraint and set multiplier from 1
For example: multiplier 0.5 will be 25% from top to center ImageView. So your multiplier will be ~0.6
By the way, there is another way how to do it:
1. Create transparent view from top to your imageView
2. Set height equal to your subview
3. Set multiplier to 0.3 to this height constraint
4. Set bottom space from your imageView to this transparent view equal to zero
In the Equal Heights Constraint properties pane, you set the multiplier to "1:3" (i.e. 30% in division notation).
To avoid having to recalculate a constant each time after layoutSubviews, use UILayoutGuide.
Create a layout guide equal to 30% of the view's height, and then use that to align the top of the child view. No manual layout calculations necessary.
// Create a layout guide aligned with the top edge of the parent, with a height equal to 30% of the parent
let verticalGuide = UILayoutGuide()
parent.addLayoutGuide(verticalGuide)
verticalGuide.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
verticalGuide.heightAnchor.constraint(equalTo: parent.heightAnchor, multiplier: 0.3).isActive = true
// Align the top of the child to the bottom of the guide
child.topAnchor.constraint(equalTo: verticalGuide.bottomAnchor).isActive = true
UILayoutGuide can be laid out with constraints like any view, but doesn't appear in the view hierarchy.
Here is Simple Solution if you want to give Constraint according to Screen.
To Set Height Percentage
To Set Width Percentage
import UIKit
extension NSLayoutConstraint{
/// Set Constant as per screen Width Percentage
#IBInspectable var widthPercentage:CGFloat {
set {
self.constant = (UIScreen.main.bounds.size.width * newValue)/100
}
get {
return self.constant
}
}
/// Set Constant as per screen Height Percentage
#IBInspectable var heightPercentage:CGFloat {
set {
self.constant = (UIScreen.main.bounds.size.height * newValue)/100
}
get {
return self.constant
}
}
}

How do I retrive the value of a constraint?

I have a NSLayoutConstraint constraint:
var myConstantLeading: CGFloat = 10
var myConstantTrailing: CGFloat = 10
var myConstraintLeading = NSLayoutConstraint (item: image,
attribute: NSLayoutAttribute.Leading,
relatedBy: NSLayoutRelation.Equal,
toItem: self.view,
attribute: NSLayoutAttribute.Leading,
multiplier: 1,
constant: myConstantLeading)
self.view.addConstraint(myConstraintLeading)
var myConstraintTrailing = NSLayoutConstraint (item: image,
attribute: NSLayoutAttribute.Trailing,
relatedBy: NSLayoutRelation.Equal,
toItem: self.view,
attribute: NSLayoutAttribute.Trailing,
multiplier: 1,
constant: myConstantTrailing)
self.view.addConstraint(myConstraintTrailing)
When an UIButton is pressed, the image gets scaled:
self.image.transform = CGAffineTransformMakeScale(0.8, 0.8)
Though, after the transformation finishes, the constant doesn't change:
println(myConstraint.constant) // equals to 10
println(myConstant) // equals to 10
I resized the image, hence the constants should vary. Why isn't that happening?
The constraints you've set both use "Equal" relationships with a constant of 10pt from either side. If you change the image size without adjusting the constraints you've violated the constraint requirements & have an ambiguous layout.
If you want to observe the change in constant values they need to be allowed to change. You have to change "Equal" to "Greater Than Or Equal" so the constraint is allowed to vary from its current 10pt value. This of course assumes that the image can only shrink - it will never be larger than 10pt away from the edge, but it could be smaller.
You also still need to clearly define what you want to happen to the layout after transforming the image. If you want the image to remain centered after the button tap/resize, you would ideally just add a constraint to the image to center it horizontally in the container.
Adjusting only "=" to ">=" would still be ambiguous, because the system doesn't know how far from left or right the image should be, nor what the image width will be. You need to give it more information, such as "center horizontally in container" AND "leading & trailing edges >= 10pt from the superview".
If you look at UIView Class Reference transform property you see a warning stating :
If this property is not the identity transform, the value of the frame property is undefined and therefore should be ignored.
and since your view transform property is not the identity transform and its frame is undefined then all its constraints constants should be ignored as well because they are linked with view's frame

Constraints messing up frame size

I use the following line of code to set the size of a button:
self.toolsButton.frame.size = CGSizeMake(190, 40)
All is fine, until I add the following layout constraint:
var constrainToCenter = NSLayoutConstraint(item: toolsButton, attribute: .CenterX, relatedBy: .Equal, toItem: self.view, attribute: .CenterX, multiplier: 1.0, constant: 0.0)
self.view.addConstraint(constrainToCenter)
As I understand it, this constraint code horizontally centers the button with the view, but why would that have an effect on the frame size? How can I maintain the frame size while also having the constraint?
You can add a height and a width constraint to enforce the size of the button.
If you are using constraints, it's best if your constraints completely define the size and position of the element.
When you add constraints for just one attribute, it can conflict with the default system constraints.
Edit - some more information:
If you create the view in code, the view.translatesAutoresizingMaskIntoConstraints attribute should default to YES.
This means that the system will automatically add constraints to it. And probably they conflict with the constraint you added manually.
So the best solution here, I think, is to set
view.translatesAutoresizingMaskIntoConstraints = NO
and then add 4 constraints manually:
horizontal position
vertical position
height
width
When you don't need to create any specific constraints, just leave that option to YES, the system will create constraints to enforce the frame you set and often that will be enough.

Resources