Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS Autolayout adaptive UI problems

I want to change my layout depending on if the width is greater than the height. But I keep getting layout warnings. I watch both WWDC videos on adaptive layout which helped a lot but did not solve the problem. I've created a simple version of my layout in using the following code. This is only so people can reproduce my issue. Code is run on the iPhone 7 plus.

import UIKit

class ViewController: UIViewController {

    var view1: UIView!
    var view2: UIView!

    var constraints = [NSLayoutConstraint]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view1 = UIView()
        view1.translatesAutoresizingMaskIntoConstraints = false
        view1.backgroundColor = UIColor.red

        view2 = UIView()
        view2.translatesAutoresizingMaskIntoConstraints = false
        view2.backgroundColor = UIColor.green

        view.addSubview(view1)
        view.addSubview(view2)

        layoutPortrait()
    }

    func layoutPortrait() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a2]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
        NSLayoutConstraint.activate(constraints)
    }

    func layoutLandscape() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1(a2)][a2(a1)]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a2]|", options: [], metrics: nil, views: views)
    NSLayoutConstraint.activate(constraints)
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
         super.viewWillTransition(to: size, with: coordinator)

         NSLayoutConstraint.deactivate(constraints)

         constraints.removeAll()

         if (size.width > size.height) {
             layoutLandscape()
         } else {
            layoutPortrait()
         }

     } 

}

When I rotate a couple times xcode logs the warnings. I think I do the layout switch to early because it is still changing but I already set the height in portrait to big or something. Does someone know what I do wrong?

Constraint warnings: (happens when going back from landscape to portrait)

[LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x618000081900 V:|-(0)-[UIView:0x7ff68ee0ac40]   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>",
    "<NSLayoutConstraint:0x618000083cf0 V:[UIView:0x7ff68ee0ac40]-(0)-[UIView:0x7ff68ee0ade0]   (active)>",
    "<NSLayoutConstraint:0x618000083d40 V:[UIView:0x7ff68ee0ade0]-(0)-|   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>
like image 565
Haagenti Avatar asked Jan 15 '17 01:01

Haagenti


2 Answers

You are correct you are doing the constraint work too early. The <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414... is a system-created constraint for the height of the view controller's view in landscape - why it's 414, exactly iPhone 5.5 (6/7 Plus) height in landscape. The rotation hasn't started, the vertical constraints with the 450-height constraint conflict, and you get the warning.

So you have to add the constraints after the rotation has completed. viewWillLayoutSubviews is the logical place, as this gets called when the view size is ready but before anything appears on screen, and can save the system the layout pass it would have made.

However, to silence the warning you still need to remove the constraints in viewWillTransition(to size: with coordinator:). The system does new auto layout calculations before viewWillLayoutSubviews() is called, and you will get the warning the other way, when going from portrait to landscape.

EDIT: As @sulthan noted in the comment, since other things can trigger layout you should also always remove constraints before adding them. If [constraints] array was emptied there's no effect. Since emptying the array after deactivating is a crucial step it should be in its own method, so clearConstraints().

Also, remember to check for orientation in initial layout - you call layoutPortrait() in viewDidLoad() thought of course that wasn't the issue.

New/change methods:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    // Should be inside layoutPortrait/Landscape methods, here to keep code sample short.
    clearConstraints() 

    if (self.view.frame.size.width > self.view.frame.size.height) {
        layoutLandscape()
    } else {
        layoutPortrait()
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    clearConstraints()
}

/// Ensure array is emptied when constraints are deactivated. Can be called repeatedly. 
func clearConstraints() {
    NSLayoutConstraint.deactivate(constraints) 
    constraints.removeAll()
}
like image 87
Mike Sand Avatar answered Nov 14 '22 10:11

Mike Sand


If the layout constraint problem is due to a fixed (or programmatically modified) width or height constraint conflicting with a UIView-Encapsulated-Layout-Width or UIView-Encapsulated-Layout-Height constraint, it's a problem of that unbreakable constraint causing your too-big, required (priority 1000) constraint to be tossed.

You can frequently fix the problem by simply lowering the priority of your conflicting constraint (perhaps just to 999) which will accommodate any temporary conflict until rotation is completed and the subsequent layout will proceed with the width or height you need.

like image 22
JBJr Avatar answered Nov 14 '22 09:11

JBJr