What Happens with Constraints When a View Is Removed

What happens with constraints when a view is removed

The constraints are removed. If you add A again, you will have to make new constraints for it, or if you save the constraints before you remove A, you can add them back. When I do something like this, I save the constraints like this for a view called view1:

self.portraitConstraints = [NSMutableArray new];
for (NSLayoutConstraint *con in self.view.constraints) {
if (con.firstItem == self.view1 || con.secondItem == self.view1) {
[self.portraitConstraints addObject:con];
}
}

What happens with constraints when a view is begin to be hidden?

Normally, hiding a view (by setting isHidden) has no effect on layout. Hidden views participate in layout. Any constraints connected to the view are still enforced. The area occupied by the now-hidden view is still reserved for it.

This is useful because it allows you to use hidden views as “spacers” to create layouts (in Interface Builder) that you cannot otherwise create. (In code, you can use UILayoutGuides instead of hidden views, but IB doesn't support creating layout guides.)

UIStackView is different from other views. UIStackView observes the isHidden property of each of its arranged subviews. When an arranged subview's isHidden changes, the UIStackView updates constraints to make or remove the area used by that subview.

No other view does what UIStackView does, so no other view adjusts the layout of its subviews when they become hidden or visible.

remove subview' will remove the related constraint.How does iOS worked?

Each UIView has an array (constraints)of [NSLayoutConstraint]s that:

  1. Constrain the view's size (e.g. .Height);
  2. Constrain the view to one of its subviews; or
  3. Constrain two of its subviews to each other.

When a view is deleted, all of the constraints in its constraints are deleted or deactivated. Then its superview's, and its superview's superview's, constraints are examined, and any that involve the view to be deleted are deleted or deactivated.

Remove all constraints affecting a UIView

The only solution I have found so far is to remove the view from its superview:

[view removeFromSuperview]

This looks like it removes all constraints affecting its layout and is ready to be added to a superview and have new constraints attached. However, it will incorrectly remove any subviews from the hierarchy as well, and get rid of [C7] incorrectly.

How to use Auto Layout to move other views when a view is hidden?

It is possible, but you'll have to do a little extra work. There are a couple conceptual things to get out of the way first:

  • Hidden views, even though they don't draw, still participate in Auto Layout and usually retain their frames, leaving other related views in their places.
  • When removing a view from its superview, all related constraints are also removed from that view hierarchy.

In your case, this likely means:

  • If you set your left view to be hidden, the labels stay in place, since that left view is still taking up space (even though it's not visible).
  • If you remove your left view, your labels will probably be left ambiguously constrained, since you no longer have constraints for your labels' left edges.

What you need to do is judiciously over-constrain your labels. Leave your existing constraints (10pts space to the other view) alone, but add another constraint: make your labels' left edges 10pts away from their superview's left edge with a non-required priority (the default high priority will probably work well).

Then, when you want them to move left, remove the left view altogether. The mandatory 10pt constraint to the left view will disappear along with the view it relates to, and you'll be left with just a high-priority constraint that the labels be 10pts away from their superview. On the next layout pass, this should cause them to expand left until they fill the width of the superview but for your spacing around the edges.

One important caveat: if you ever want your left view back in the picture, not only do you have to add it back into the view hierarchy, but you also have to reestablish all its constraints at the same time. This means you need a way to put your 10pt spacing constraint between the view and its labels back whenever that view is shown again.

When does iOS remove inactive constraints

isActive is not a very good property name. The constraint is not actually deactivated, it is removed from the view hierarchy.

If the view had the only strong reference to the constraint, the constraint will be deallocated and you won't be able to activate it again.

That means that if you want the constraint to be kept in memory, you have to reference it strongly, e.g.:

@IBOutlet var myConstraint: NSLayoutConstraint!

not

@IBOutlet weak var myConstraint: NSLayoutConstraint?

If you keep a strong reference, the reference will never get set to nil unless you set it to nil yourself.

Why calling setNeedsUpdateConstraints isn't needed for constraint changes or animations?

This is a common misunderstanding among iOS developers.

Here's one of my "golden rules" for Auto Layout:

Don't bother about "updating constraints".

You never need to call any of these methods:

  • setNeedsUpdateConstraints()
  • updateConstraintsIfNeeded()
  • updateConstraints()
  • updateViewConstraints()

except for the very rare case that you have a tremendously complex layout which slows down your app (or you deliberately choose to implement layout changes in an atypical way).

The Preferred Way to Change Your Layout

Normally, when you want to change your layout, you would activate / deactivate or change layout constraints directly after a button tap or whichever event triggered the change, e.g. in a button's action method:

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
toggleLayout()
}

func toggleLayout() {
isCenteredLayout = !isCenteredLayout

if isCenteredLayout {
centerXConstraint.isActive = true
} else {
centerXConstraint.isActive = false
}
}

As Apple puts it in their Auto Layout Guide:

It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. Deferring these changes to a later method makes the code more complex and harder to understand.

You can of course also wrap this constraint change in an animation: You first perform the constraint change and then animate the changes by calling layoutIfNeeded() in the animation closure:

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
// 1. Perform constraint changes:
toggleLayout()
// 2. Animate the changes:
UIView.animate(withDuration: 1.8, animations: {
view.layoutIfNeeded()
}
}

Whenever you change a constraint, the system automatically schedules a deferred layout pass, which means that the system will recompute the layout in the near future. No need to call setNeedsUpdateConstraints() because you just did update (change) the constraint yourself! What needs to be updated is the layout i.e. the frames of all your views, not any other constraint.

The Principle of Invalidation

As previously stated, the iOS layout system usually doesn't react immediately to constraint changes but only schedules a deferred layout pass. That's for performance reasons. Think of it like this:

When you go shopping groceries, you put an item in your cart but you don't pay it immediately. Instead, you put other items in your cart until you feel like you got everything you need. Only then you proceed to the cashier and pay all your groceries at once. It's way more efficient.

Due to this deferred layout pass there is a special mechanism needed to handle layout changes. I call it The Principle of Invalidation. It's a 2-step mechanism:

  1. You mark something as invalid.
  2. If something is invalid, you perform some action to make it valid again.

In terms of the layout engine this corresponds to:

  1. setNeedsLayout()
  2. layoutIfNeeded()

and

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

The first pair of methods will result in an immediate (not deferred) layout pass: First you invalidate the layout and then you recompute the layout immediately if it's invalid (which it is, of course).

Usually you don't bother if the layout pass will happen now or a couple of milliseconds later so you normally only call setNeedsLayout() to invalidate the layout and then wait for the deferred layout pass. This gives you the opportunity to perform other changes to your constraints and then update the layout slightly later but all at once (→ shopping cart).

You only need to call layoutIfNeeded() when you need the layout to be recomputed right now. That might be the case when you need to perform some other calculations based on the resulting frames of your new layout.

The second pair of methods will result in an immediate call of updateConstraints() (on a view or updateViewConstraints() on a view controller). But that's something you normally shouldn't do.

Changing Your Layout in a Batch

Only when your layout is really slow and your UI feels laggy due to your layout changes you can choose a different approach than the one stated above: Rather than updating a constraint directly in response to a button tap you just make a "note" of what you want to change and another "note" that your constraints need to be updated.

@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
// 1. Make a note how you want your layout to change:
isCenteredLayout = !isCenteredLayout
// 2. Make a note that your constraints need to be updated (invalidate constraints):
setNeedsUpdateConstraints()
}

This schedules a deferred layout pass and ensures that updateConstraints() / updateViewConstraints() will be called during the layout pass. So you may now even perform other changes and call setNeedsUpdateConstraints() a thousand times – your constraints will still only be updated once during the next layout pass.

Now you override updateConstraints() / updateViewConstraints() and perform the necessary constraint changes based on your current layout state (i.e. what you have "noted" above in "1."):

override func updateConstraints() {
if isCenteredLayout {
centerXConstraint.isActive = true
} else {
centerXConstraint.isActive = false
}

super.updateConstraints()
}

Again, this is only your last resort if the layout is really slow and you're dealing will hundreds or thousands of constraints. I have never needed to use updateConstraints() in any of my projects, yet.

I hope this make things a little clearer.

Additional resources:

  • Auto Layout – From Leading to Trailing: my talk from UIKonf 2017, topics:

    • "The Layout Pass" and
    • "Updating Constraints"
  • The Auto Layout Comprehendium™: scroll down to section "Updating Constraints", maintained by me
  • The Auto Layout Guide by Apple: sections

    • "Changing Constraints"
    • "The Deferred Layout Pass"


Related Topics



Leave a reply



Submit