117

I've set up multiple sets of constraints in IB, and I'd like to programmatically toggle between them depending on some state. There's a constraintsA outlet collection all of which are marked as installed from IB, and a constraintsB outlet collection all of which are uninstalled in IB.

I can programmatically toggle between the two sets like so:

NSLayoutConstraint.deactivateConstraints(constraintsA)
NSLayoutConstraint.activateConstraints(constraintsB)

But... I can't figure out when to do that. It seems like I should be able to do that once in viewDidLoad, but I can't get that to work. I've tried calling view.updateConstraints() and view.layoutSubviews() after setting the constraints, but to no avail.

I did find that if I set the constraints in viewDidLayoutSubviews everything works as expected. I guess I'd like to know two things...

  1. Why am I getting this behavior?
  2. Is it possible to activate/deactivate constraints from viewDidLoad?
tybro0103
  • 48,327
  • 33
  • 144
  • 170
  • 2
    Do you mean the deactivateConstraints and activateConstraints worked in viewWillLayoutSubviews? I tried that, and it didn't work there or in viewDidLoad. It kind of worked in viewDidAppear; the view appeared where the new constraints should put it, but if I rotated to landscape, the view moved back to the position determined by the constraints set in IB (and stayed there when I rotated back to portrait). Logging the constraints, showed the correct ones (the ones newly activated). This seems like a bug to me. – rdelmar Dec 15 '14 at 23:40
  • @rdelmar yes it worked for me in viewWillLayoutSubviews. Perhaps the constraints you tried to activate weren't valid? Or maybe forgot to call super? – tybro0103 Dec 15 '14 at 23:45
  • 1
    Yes, they were valid (they worked in viewDidAppear), and there's no need to call super because there is no default implementation of viewWillLayoutSubviews (I tried it with calling super anyway, but that made no difference). – rdelmar Dec 15 '14 at 23:48
  • Anyway, I don't know why this shouldn't work in viewDidLoad. I've added constraints there in the past, and that works fine. – rdelmar Dec 15 '14 at 23:50
  • Weird. I hope there is a good answer. I'd love to understand this better. – tybro0103 Dec 15 '14 at 23:53
  • I tried one other thing to make sure it wasn't some timing issue. I called those two methods in a timer's action method that was called 2 seconds after viewDidLoad. I got the same results I got with viewDidAppear -- the view was in its new position, but reverted on rotation. – rdelmar Dec 15 '14 at 23:54
  • 1
    @rdelmar Just got the chance to go test more... I can verify I actually got the same behavior you described... works at first in viewDidAppear, but then reverts upon rotation. – tybro0103 Dec 16 '14 at 03:04
  • @rdelmar I wonder if when you've had success adding constraints from viewDidLoad if you were not using adaptive views, but now you are? – tybro0103 Dec 16 '14 at 04:08
  • 4
    Apparently you can't mark constraints as not installed in IB for this purpose. Found that information here: http://stackoverflow.com/questions/27663249/activateconstraints-and-deactivateconstraints-not-working and it solved the problem for me. – Stefan Jun 22 '15 at 11:07
  • 1
    I had my constraints implemented the same way as described in the question, except I activated/deactivated some of them in viewDidAppear. This worked, but you could see the elements quickly change position (a minor but undesirable issue). Making the change in viewWillAppear or viewDidLoad did not work. But after reading this question, I tried making that change in viewDidLayoutSubviews. It worked and the position change is no longer visible to the user. (It also worked in viewWillLayoutSubviews). So thanks for that tip! – peacetype Dec 20 '15 at 01:31
  • For me the only reliable way is to adjust constraints in `viewDidLayoutSubviews()`. Adjusting constraints in `viewWillLayoutSubviews()` does not work in my case. – petrsyn Nov 08 '16 at 12:02

8 Answers8

202

I activate and deactivate NSLayoutConstraints in viewDidLoad, and I do not have any problems with it. So it does work. There must be a difference in setup between your app and mine :-)

I'll just describe my setup - maybe it can give you a lead:

  1. I set up @IBOutlets for all the constraints that I need to activate/deactivate.
  2. In the ViewController, I save the constraints into class properties that are not weak. The reason for this is that I found that after deactivating a constraint, I could not reactivate it - it was nil. So, it seems to be deleted when deactivated.
  3. I do not use NSLayoutConstraint.deactivate/activate like you do, I use constraint.active = YES/NO instead.
  4. After setting the constraints, I call view.layoutIfNeeded().
Karthik Kumar
  • 1,375
  • 1
  • 12
  • 29
Joachim Bøggild
  • 2,182
  • 1
  • 10
  • 4
  • 146
    "save the constraints into class properties that are not weak" You saved me a lot of time, thanks! – OpenUserX03 Sep 21 '15 at 05:37
  • 11
    "I save the constraints into class properties that are not weak": This saved me tons of heartache. I didn't know that I was calling a selector on a nil object. Thanks!! – static0886 Dec 07 '15 at 11:22
  • 5
    Important to note that "inactive" constraints aren't ignored by auto layout, they are removed. Activating/deactivating constraints actually adds them and removes them. Spent some time debugging a conflicting auto layout after I added the constraints that I had previously set `.active = false` expecting they would be ignored until I set them to active. – lbarbosa May 03 '16 at 17:52
  • 1
    save the constraints into class properties that are not weak, ok this saves a lot of time, i was getting some mixed results without this. Thanks man! – MegaManX Jul 04 '16 at 10:07
  • just remove 'weak' from the IBOutlet property declaration – dRamentol Aug 10 '16 at 15:10
  • 4
    Apples doc says: Activating or deactivating the constraint calls addConstraint(_:) and removeConstraint(_:) on the view that is the closest common ancestor of the items managed by this constraint. Use this property instead of calling addConstraint(_:) or removeConstraint(_:) directly. Thus it seems that when a constraint is deactivated it is removed and then there are no strong references to the constraint left, unless the IBOutlet is strong. Hence the constraint is deleted. IMHO this is almost a bug or at least very unexpected behavior. – Olle Raab Dec 12 '16 at 13:28
  • 1
    User stefan submitted an explanation in his comment to the question, marking constraints as not installed in IB is specifically for use only with size classes, not for designing constraints that are deactivated when the view is instantiated. It almost works, but there's a subtle bug or incompatibility that leads to what the OP is seeing. You must leave all the constraints installed in IB, either that or add the deactivated constraints in code instead. In my app the viewDidLayoutSubviews solution didn't work, not using uninstalled constraints did. See https://stackoverflow.com/a/27663981/592739 – Pierre Houston Oct 04 '18 at 23:58
62

Maybe you could check your @properties, replace weak with strong.

Sometimes it because active = NO set self.yourConstraint = nil, so that you couldn't use self.yourConstraint again.

shim
  • 9,289
  • 12
  • 69
  • 108
Leo
  • 1,545
  • 13
  • 24
  • 9
    As stated in the [Swift Language Guide](https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID48), properties are strong by default so you can also just remove `weak` and that will do it. – Jonathan Cabrera Aug 14 '18 at 15:42
29
override func viewDidLayoutSubviews() {
// do it here, after constraints have been materialized
}
Tony Swiftguy
  • 444
  • 3
  • 3
15

I believe the problem you are experiencing is due to constraints not being added to their views until AFTER viewDidLoad() is called. You have a number of options:

A) You can connect your layout constraints to an IBOutlet and access them in your code by these references. Since the outlets are connected before viewDidLoad() kicks off, the constraints should be accessible and you can continue to activate and deactivate them there.

B) If you wish to use UIView's constraints() function to access the various constraints you must wait for viewDidLayoutSubviews() to kick off and do it there, since that is the first point after creating a view controller from a nib that it will have any installed constraints. Don't forget to call layoutIfNeeded() when you're done. This does have the disadvantage that the layout pass will be performed twice if there are any changes to apply and you must ensure that there is no possibility that an infinite loop will be triggered.

A quick word of warning: disabled constraints are NOT returned by the constraints() method! This means if you DO disable a constraint with the intention of turning it back on again later you will need to keep a reference to it.

C) You can forget about the storyboard approach and add your constraints manually instead. Since you're doing this in viewDidLoad() I assume that the intention is to only do it once for the full lifetime of the object rather than changing the layout on the fly, so this ought to be an acceptable method.

Ash
  • 9,064
  • 3
  • 48
  • 59
15

You can also adjust the priority property to "enable" and "disable" them (750 value to enable and 250 to disable for example). For some reason changing the active BOOL didn't had any effect on my UI. No need for layoutIfNeeded and can be set and changed at viewDidLoad or any time after that.

user2387149
  • 1,219
  • 16
  • 28
  • A very good suggestion. Changing constraint priority works in `viewWillTransition(to:, with:)` or `viewWillLayoutSubviews()` and you can keep all your alternative constraints as "installed" in a storyboard. Constraint priority may not change from non-required to required, so use values below `1000`. On the other hand, activating (adding) and deactivating (removing) constraints works only in `viewDidLayoutSubviews()` and requires keeping `strong` `@IBOutlet` references to `NSLayoutConstraint`-s. – Gary Aug 10 '17 at 20:07
  • "For some reason changing the active BOOL didn't had any effect on my UI". Based on [here](http://indiestack.com/2016/03/constraint-activation/). I _think_ you can't change a constraint with a 1000 priority during runtime. If you want to deactive it, then you should set the initial priority to 999 or lower.... – mfaani Nov 28 '17 at 16:10
  • I disagree with this statement as it can lead to hard to debug problems and does not answer the question. Setting the priority to 250 will not "deactivate" the constraint, it will still have effect and influence the layout. It may seem like it "deactivates" the constraint in most cases but definitely not in all cases. (in particular, not the case that lead me to find answer to this question) – Tumata Apr 05 '19 at 21:23
  • 1
    It might lead to crashes as well like "Mutating a priority from required to not on an installed constraint (or vice-versa) is not supported. You passed priority 250 and the existing priority was 1000." – Karthick Ramesh Nov 07 '19 at 20:13
8

The proper time to deactivate unused constraints:

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];

    self.myLittleConstraint.active = NO;
}

Keep in mind that viewWillLayoutSubviews could be called multiple times, so no heavy calculations here, okay?

Note: if you want to reactive some of the constraints later, then always store strong reference to them.

Laszlo
  • 2,803
  • 2
  • 28
  • 33
  • 2
    For me the only reliable way is to adjust constraints in `viewDidLayoutSubviews()`. Adjusting constraints in `viewWillLayoutSubviews()` does not work in my case. – petrsyn Nov 08 '16 at 12:02
5

When a view is being created the following life cycle methods are called in order:

  1. loadView
  2. viewDidLoad
  3. viewWillAppear
  4. viewWillLayoutSubviews
  5. viewDidLayoutSubviews
  6. viewDidAppear

Now to your questions.

  1. Why am I getting this behavior?

Answer: Because when you try to set the constraints on the views in viewDidLoad the view does not have its bounds, hence constraints cannot be set. It's only after viewDidLayoutSubviews that the view's bounds are finalized.

  1. Is it possible to activate/deactivate constraints from viewDidLoad?

Answer: No. Reason explained above.

peacetype
  • 1,928
  • 3
  • 29
  • 49
Sumeet
  • 1,055
  • 8
  • 18
  • In your description of the viewController lifecycle you talked about how the view is first loaded and then viewDidLoad is called. Yet you also said the view is not created by the time viewDidLoad is called, this is clearly a contradiction. Also, you can test for yourself and see that the view has been created by the time viewDidLoad is called because you can add subviews to the view. – ABakerSmith Apr 03 '15 at 22:28
  • viewDidLoad should be fine since views are created and loaded... In reality where you activate constraints mainly comes down to performance. I'm guessing the original problem was unrelated to where the constraints were activated. http://stackoverflow.com/questions/19387998/where-should-i-be-setting-autolayout-constraints-when-creating-views-programatic – Gabe Nov 06 '15 at 19:36
  • @ABakerSmith i have edited my answer to be more clear. – Sumeet Nov 13 '15 at 06:36
1

I have found as long as you set up the constraints per normal in the override of - (void)updateConstraints (objective c), with a strong reference for the initiality used active and un-active constraints. And elsewhere in the view cycle deactivate and/or activate what you need, then calling layoutIfNeeded, you should have no issues.

The main thing is not to constantly reuse the override of updateConstraints and to separate the activations of the constraints, as long as you call updateConstraints after your first initialization and layout. It does seem to matter after that where in the view cycle.

elliotrock
  • 3,410
  • 3
  • 22
  • 25