Joe Blow Joe Blow - 5 months ago 28
iOS Question

Terrifyingly, layoutIfNeeded sequence acts differently on iPad versus iPhone; how to fix?

Fire up Xcode and for clarity build only to say 9.3, universal app. So, compare 9.3 iPads with 9.3 iPhones. Build to both simulator and devices - issue exhibits on both.

The app rotates in all four directions.

Have a typical situation where you do something like this...

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!

var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }

let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction

self.view.layoutIfNeeded() // holy! read on....
}
}


Notice the
layoutIfNeeded()
after the change to the constraint.

Continuing the typical example, you will have something like

override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
heightFraction = (heightFraction)
// use "autolayout power" for perfection every pass.

// now that basic height/position is set,
save/load reactive positions...
position detail stuff...
}


Check it out ... I was doing this all day and only happened to use iPhones.

Interestingly you DO NOT NEED the layoutIfNeeded call.

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
}
}


Works fine.

However at the end of the day I put it on some iPads and .... everything broke!

Whenever you rotate landscape/portrait, problems.

After a head scratch, I realized that incredibly YOU DO NEED the layoutIfNeeded call, on iPad. That's on the IDENTICAL OS.

Indeed the behavior exhibits regardless of OS version. And it exhibits for ALL iPhones / ALL iPads. WTF!

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
self.view.layoutIfNeeded() //MUST HAVE, IN IPAD CASE!!!!!!
}
}


To me it is incredibly troubling that they would work differently.

What I'm wondering is, is there perhaps a setting somewhere to make them work the same? Could it be my fault somehow?

Are there any other know differences between the two - or indeed is it "known" that there are a few bugs like this?

I can't think of anything odd or unusual I did anywhere, except the whole app has
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return .All }
in the first view as is normal if you want to turn the device upside down; I doubt its related. Other than that it's a very "clean" fresh app.

It gave me a glitch-in-the-matrix feeling - it was terrifying.

What could cause this?




Per RobM's question, the SimulatedMetrics settings (Attributes tab) on the initial ViewController are...

enter image description here




General scheme of the app: the first scene "General" is full-screen, the size of the device. There's a container to "Live" which is the same size (using "Trailing" etc/ constraints as zero all round). In Live, there's a container view "Quad" which indeed is also fully sized to "Live," so it's also fullscreen. Quad:UIViewController exhibits the issue I describe. Quad contains various objects (images, custom controls etc) which sit around on the view. When the app launches, all is fine.

On rotation of the device (or similar): just after the change to the constraint (I don't know if that's relevant): the
layoutIfNeeded
call IS needed for iPads (all iPads), but is NOT needed for iPhones (all iPhones). The behavior is identical in the simulator and on devices.

Answer

I'm not able to reproduce what you're seeing; it would be nice to see a complete example. In my mockup I configured a view controller with a view having a single subview, with a constraint to control the subview height. I altered the subview height constraint in viewDidLayout based on the view size. The behavior was identical for both iPhone and iPad, and worked sans calling layoutIfNeeded on any view.

That said, I think you're changing subview constraints once the view has completed its layout - yes? I think the better way to do that would be to layout your subviews ahead of that, via viewWillTransitionToSize:withTransitionCoordinator:.

func viewWillTransitionToSize(_ size: CGSize,
    withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)

This way auto layout for the view hierarchy can complete in a single pass. This method is only called when the view is changing size, so it won't be called when the view is first loaded; you'll have to set up your initial constraints somewhere else - since they're dependent on view size perhaps you can use viewWillAppear.

Alternatively, (and possibly more correctly), subclass your view controller's view and override updateConstraints. This is the most appropriate place for changing your constraint constants.

Finally, in your property setter, you shouldn't ever call view.layoutIfNeeded(). If anything, you can set view.setNeedsLayout() so that layout happens in the next runloop iteration, and picks up ALL changes that may need to be represented.