Setting constraint "isActive=false" causes fatal error

I have an imageView which has a top constraint IBOutlet called imageViewTopConstraint. In viewWillLayoutSubviews( ) I set the imageViewTopConstraint.constant value. The imageView displays properly for all iPhone models while in portrait mode, which is good.


When I switch to landscape mode, I want to disable the imageViewTopConstraint, so the viewWillLayoutSubviews( ) has the following code:


if UIDevice.current.orientation.isPortrait {
   imageViewTopConstraint.constant = (2 * screenHeight) / 3
   imageViewTopConstraint.isActive = true
}
else if UIDevice.current.orientation.isLandscape {
   imageViewTopConstraint.isActive = false
}


Issue

When I run the application it is initially in portrait mode. When I switch to landscape mode I receive the following fatal error at the line where I set "imageViewTopConstraint.isActive = false" to disable the constraint (line 6 above)


Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value


Any idea what is going on or what this means?

Accepted Reply

The problem is that viewWilllayout is called several times.


Second time, you have set the constraint as inactive, which means it has been removed (it will be reinstalled when you set to true).



So one way is to test


if UIDevice.current.orientation.isPortrait {
   imageViewTopConstraint.constant = (2 * screenHeight) / 3
   imageViewTopConstraint.isActive = true
}
else if UIDevice.current.orientation.isLandscape {
   if imageViewTopConstraint != nil { imageViewTopConstraint.isActive = false }
}

Replies

To be sure of what happens, please add some log:


if UIDevice.current.orientation.isPortrait {
   imageViewTopConstraint.constant = (2 * screenHeight) / 3
   print("isPortrait  imageViewTopConstraint",  imageViewTopConstraint)
   imageViewTopConstraint.isActive = true
}
else if UIDevice.current.orientation.isLandscape {
   print("isLandscape  imageViewTopConstraint before",  imageViewTopConstraint)
   imageViewTopConstraint.isActive = false
   print("isLandscape  imageViewTopConstraint after",  imageViewTopConstraint)
}


and tell what youi get when you turn landscape. Notably if this is called several times.


Probably, you have removed the constraint somewhere making it nil.

hi uncletr,


this sounds familiar to me ... a constraint reference unexpectedly being nil when trying to tweak a layout between portrait and landscape.


please let us know how the constraint is defined: is it defined in IB and then referenced from your code, or is it defined and added directly in code, say in viewDidLoad()?


in my recent case, i had a reference to a constraint defined in IB; when i set it to active = false, the constraint reference immediately became nil, almost exactly as you describe.


looking forward to more info,

DMG

DelawareMathGuy,


I defined the constraint in IB (using the little tie-fighter icon), then highlighted the constraint and dragged it into the code and assigned an IBOutlet name to the constraint.


>>when i set it to active = false, the constraint reference immediately became nil

I believe this is exactly what is happening to me (please read below). This feels wrong to me.


Claude:


If I add the "print" statements like you requested (as below) ...


print("isPortrait imageViewTopConstraint", imageViewTopConstraint)


then I see the following warnings produced for that "print" line


Coercion of implicitly unwrappable value of type 'NSLayoutConstraint?' to 'Any' does not unwrap optional

Provide a default value to avoid this warning

Force-unwrap the value to avoid this warning

Explicitly cast to 'Any' with 'as Any' to silence this warning


That being said, since these are just warnings, I ran the code anyway.


Results


ViewWillLayoutSubviews

isPortrait seatPos1TopAlignment_Constraint = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)


ViewWillLayoutSubviews

isPortrait seatPos1TopAlignment_Constraint = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)


ViewWillLayoutSubviews

isPortrait seatPos1TopAlignment_Constraint = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)


ViewWillLayoutSubviews

isPortrait seatPos1TopAlignment_Constraint = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)


ViewWillLayoutSubviews

isPortrait seatPos1TopAlignment_Constraint = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)


ViewWillLayoutSubviews

isLandscape seatPos1TopAlignment_Constraint before = Optional(<NSLayoutConstraint:0x600000a53c00 UIView:0x7f9d937659f0.top == UILayoutGuide:0x60000107cb60'UIViewSafeAreaLayoutGuide'.top + 313.033 (active)>)

isLandscape seatPos1TopAlignment_Constraint after = nil


ViewWillLayoutSubviews

isLandscape seatPos1TopAlignment_Constraint before = nil

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file /Users/unclet/Dads Stuff/Swift Projects/Backup/OhHell 3/OhHell/GameplayViewController.swift, line 3313



QUESTION

You were correct, the constraint moved to a NIL state, however, I am still not sure how it gets set to NIL. Does setting the constraint "isActive = false" make the constraint NIL? This does not seem correct.

The problem is that viewWilllayout is called several times.


Second time, you have set the constraint as inactive, which means it has been removed (it will be reinstalled when you set to true).



So one way is to test


if UIDevice.current.orientation.isPortrait {
   imageViewTopConstraint.constant = (2 * screenHeight) / 3
   imageViewTopConstraint.isActive = true
}
else if UIDevice.current.orientation.isLandscape {
   if imageViewTopConstraint != nil { imageViewTopConstraint.isActive = false }
}

Thanks for the support Claude.


Please confirm:


1) To avoid setting isActive=false multiple times, I would need to check whether the constraint is NIL before setting isActive=false inside the landscape "if" statement again.


2) Inside the portrait "if statement", I would need to first set isActive=true before setting the constraint.constant value again so the constraint can be created again in the system before setting it


PS: I still believe setting "isActive = false" should not make the constraint NIL as this makes no sense. Having a "constraint.installed" property associated with the constraint would be better for setting the constraint to NIL. The term "isActive" typically refers to "enabled/disabled" settings and not associated with "removal/deletion". My two cents ... for what it is worth.

I cannot be 100% sure that iOS behaves exactly like this, so it's more a rule of thumb than a definite statement…


Here is what doc says : https://developer.apple.com/documentation/uikit/nslayoutconstraint/1527000-isactive

deactivating the constraint calls

removeConstraint(_:)

___________________

Declaration

var isActive: Bool { get set }

Discussion


You can activate or deactivate a constraint by changing this property. Note that only active constraints affect the calculated layout. If you try to activate a constraint whose items have no common ancestor, an exception is thrown. For newly created constraints, the

isActive
property is
false
by default.

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.

_________________________



1) To avoid setting isActive=false multiple times, I would need to check whether the constraint is NIL before setting isActive=false inside the landscape "if" statement again.

Yes. What you have to avoid is calling a property on a constraint that has been removed


2) Inside the portrait "if statement", I would need to first set isActive=true before setting the constraint.constant value again so the constraint can be created again in the system before setting it

Logically, yes. Did you try both ?


PS: I still believe setting "isActive = false" should not make the constraint NIL as this makes no sense. Having a "constraint.installed" property associated with the constraint would be better for setting the constraint to NIL. The term "isActive" typically refers to "enabled/disabled" settings and not associated with "removal/deletion". My two cents ... for what it is worth.

I agrree, it is confusing. But I think that was needed for the constraint manager to work more efficiently (my 1 cent here!).

thanks for your help !

To fix it, I removed the weak keyword from the constraint definition.

@IBOutlet weak var willCrash: NSLayoutConstraint!
@IBOutlet      var worksFine: NSLayoutConstraint!

XCode by default creates any @IBOutlet as weak, which means that the object is immediately deleted when there are no other objects with a strong reference to that. So when you deactivate the constraint, it probably removes some references, and because there's no strong reference to the constraint, it will free that memory. So when it's called again, it crashes.