Weird behaviour of a NSLayoutConstraint used with Size Class variation when app going background

Hello

I set a few constraints on a UIView in the storyboard. The height constraint is set to 300 and has a size class variation set on 350 (see screenshot).

In my code, I modify the constraint and set its constant to 100. When I call that code, I see the view changing correctly its height to 100. But If I go to homescreen ( app in background) and come back to the app, the height of the view is set back to the old value (300). This problem only appears when the constraint has a size variation. When I remove the variation on the height, the behaviour is ok, the height stays at 100 when I go to homeview and switch back to the app. Is that a bug of AutoLayout ? Is there a workaround ?

(tested with iOS16, on iPhone Xr and iPhone 14 Pro, xcode 14)

class ViewController: UIViewController {
    @IBOutlet weak var heightConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func buttonPushed(_ sender: Any) {
        heightConstraint.constant = 100; // works ok until app goes background
    }
}

Accepted Reply

That means that we should never modify NSLayoutConstraint.constant in the code... or else save the values and restore them in viewDidLayoutSubviews.

My solution would be to create the troublesome constraint in code and convert the existing storyboard constraint to a design-time placeholder. Then no further workaround is needed.

@IBOutlet var myView: UIView! // btw, ‘weak’ is not usually needed
var heightConstraint: NSLayoutConstraint! // no longer an outlet

override func viewDidLoad() {
    super.viewDidLoad()
    let h = traitCollection.horizontalSizeClass == .regular ? 350 : 300
    heightConstraint = myView.heightAnchor.constraint(equalToConstant: h)
    heightConstraint.isActive = true
}

Replies

If your app goes into the background the system can kill it. How long are you leaving it in the background?

Put this line in viewDidLoad() and see what happens when you bring the app to the foreground: heightConstraint.constant = 100. Maybe also put a print("viewDidLoad was executed") in there to see that the method is called.

You're only setting the constraint's constant when you press a button, so maybe the redraw of the view is setting it back to what the storyboard says to use?

(The above isn't a fix, it's a diagnostic to see where the issue lies. You'll have to figure out how to restore the state if the above is proven correct.)

  • The app stays 5 sec in the background. It's definitely not killed (viewDidLoad is called once). Your second point is correct, however, it do work when Size Class variation is not used. It seems that the variation somehow forces the view to recalculate the layout every time the view appears, using the storyboard values.

Add a Comment

Here’s what’s happening. When you go to background, UIKit temporarily changes your traitCollection.userInterfaceStyle to the opposite (from light to dark, or from dark to light) in order to capture an app switcher screenshot in that style, and then changes it back. You can override traitCollectionDidChange: and see that it gets called twice.

Unfortunately, this trait collection change triggers a reload from the storyboard of any constraints that have size class variations, even though the size class didn’t actually change. I don’t know if this is a bug or a feature but it’s been like this for a long time.

You can observe this effect without even backgrounding: just change the device’s light/dark mode while your app is running. When you change it, the constraint will revert back to the storyboard definition.

  • That looks like a bug to me, or undocumented bad side effect. That means that we should never modify NSLayoutConstraint.constant in the code... or else save the values and restore them in viewDidLayoutSubviews.

Add a Comment

That means that we should never modify NSLayoutConstraint.constant in the code... or else save the values and restore them in viewDidLayoutSubviews.

My solution would be to create the troublesome constraint in code and convert the existing storyboard constraint to a design-time placeholder. Then no further workaround is needed.

@IBOutlet var myView: UIView! // btw, ‘weak’ is not usually needed
var heightConstraint: NSLayoutConstraint! // no longer an outlet

override func viewDidLoad() {
    super.viewDidLoad()
    let h = traitCollection.horizontalSizeClass == .regular ? 350 : 300
    heightConstraint = myView.heightAnchor.constraint(equalToConstant: h)
    heightConstraint.isActive = true
}