iosswiftautolayoutadaptive-layout

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)>

Solution

  • 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()
    }