How to make UIKit squish your views so that they can fit into the screen

Viewed 36

I have five UIView instances constrained using autolayout. When in portrait mode they fit into the screen, but when in landscape mode the screen becomes to small to contain all the views.

I know one way to solve this by creating two constraints and active one in portrait mode and deactivate the other and vice versa in landscape mode like this:

class ViewController : UIViewController{
  var portraitHeight : NSLayoutConstraint!
  var landscapeHeight : NSLayoutConstraint!

  viewDidLoad(){
    super.viewDidLoad()
    portraitHeight = firstView.heightAnchor.constraint(equalToConstant: 88)
    landscapeHeight = firstView.heightAnchor.constraint(equalToConstant: 68)
  }

 override func viewWillLayoutSubviews() {
    let orientation = UIDevice.current.orientation
    if orientation == .portrait
    {
        landscapeHeight.isActive = false
        portraitHeight.isActive = true
        
    }else if orientation == .landscapeLeft || orientation == .landscapeRight
    {
        portraitHeight.isActive = false
        landscapeHeight.isActive = true
       
    }
  }
}

But what I really want is to be able to squish the views when there is not enough space. I tried changing UILayoutPriority but it didn't work.

These are my constraints:

NSLayoutConstraint.activate([
        firstView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        firstView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
        firstView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
        firstView.heightAnchor.constraint(equalToConstant: 88),
        
        secondView.topAnchor.constraint(equalTo: firstView.bottomAnchor),
        secondView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
        secondView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
        secondView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
        
        thirdView.topAnchor.constraint(equalTo: secondView.bottomAnchor),
        thirdView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
        thirdView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
        thirdView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
        
        fourthView.topAnchor.constraint(equalTo: thirdView.bottomAnchor),
        fourthView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
        fourthView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
        fourthView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
        
        fifthView.topAnchor.constraint(equalTo: fourthView.bottomAnchor),
        fifthView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
        fifthView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
        fifthView.heightAnchor.constraint(equalTo: firstView.heightAnchor),

    ])

Desired output:

enter image description here

enter image description here

2 Answers

We can add multiple Height constraints to the first view -- as long as they don't conflict.

So, we want to tell the first view TRY to have a Height of 88-points. We'll give that constraint a less-than-required priority so auto-layout can break it if necessary:

let firstHeight: NSLayoutConstraint = firstView.heightAnchor.constraint(equalToConstant: 88.0)
firstHeight.priority = .required - 1

We don't want it to ever be shorter than 88-points, so we'll give it a second, required height constraint of less-than-or-equal to 88:

firstView.heightAnchor.constraint(lessThanOrEqualToConstant: 88.0)

and, to prevent the fifth view from extending past the safe-area bottom, we'll give it a less-than-or-equal constraint:

fifthView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)

Here's a complete example:

class FiveViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let firstView = UIView()
        firstView.backgroundColor = .red
        let secondView = UIView()
        secondView.backgroundColor = .cyan
        let thirdView = UIView()
        thirdView.backgroundColor = .yellow
        let fourthView = UIView()
        fourthView.backgroundColor = .green
        let fifthView = UIView()
        fifthView.backgroundColor = .orange

        [firstView, secondView, thirdView, fourthView, fifthView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }

        // we want the first view to TRY to be 88-points tall
        let firstHeight: NSLayoutConstraint = firstView.heightAnchor.constraint(equalToConstant: 88.0)
        // but with less-than-required priority so it can shrink
        firstHeight.priority = .required - 1
        
        
        NSLayoutConstraint.activate([
            
            firstView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            firstView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
            firstView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),

            // don't do this
            //firstView.heightAnchor.constraint(equalToConstant: 88),

            firstHeight,
            
            // we don't want the view heights to be Greater-Than 88-points
            firstView.heightAnchor.constraint(lessThanOrEqualToConstant: 88.0),

            secondView.topAnchor.constraint(equalTo: firstView.bottomAnchor),
            secondView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
            secondView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
            secondView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
            
            thirdView.topAnchor.constraint(equalTo: secondView.bottomAnchor),
            thirdView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
            thirdView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
            thirdView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
            
            fourthView.topAnchor.constraint(equalTo: thirdView.bottomAnchor),
            fourthView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
            fourthView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
            fourthView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
            
            fifthView.topAnchor.constraint(equalTo: fourthView.bottomAnchor),
            fifthView.leftAnchor.constraint(equalTo: firstView.leftAnchor),
            fifthView.rightAnchor.constraint(equalTo: firstView.rightAnchor),
            fifthView.heightAnchor.constraint(equalTo: firstView.heightAnchor),
            
            // we want the fifth view Bottom to never extend below the safe area Bottom
            fifthView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor),
            
        ])
    }
}

and the results:

enter image description here

enter image description here

The key here is indeed to make some constraints have less priority. Namely, for each view, the constraint "height=88" should have a non-required priority, so that it can be broken when the screen height is not enough. On the other hand, you should add another "height <= 88" required constraint.

let view1 = UIView()
view1.backgroundColor = .yellow
let view2 = UIView()
view2.backgroundColor = .blue
let view3 = UIView()
view3.backgroundColor = .brown
let view4 = UIView()
view4.backgroundColor = .cyan
let view5 = UIView()
view5.backgroundColor = .green

let views = [view1, view2, view3, view4, view5]
for view in views {
    view.translatesAutoresizingMaskIntoConstraints = false
    let heightConstraint = view.heightAnchor.constraint(equalToConstant: 88)
    heightConstraint.priority = .defaultHigh
    heightConstraint.isActive = true
    view.heightAnchor.constraint(greaterThanOrEqualToConstant: 88).isActive = true
}

Rather than adding lots of constraints relating the 5 views, I strongly recommend that you use a stack view for this.

let stackView = UIStackView(arrangedSubviews: views)
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)

Since you want the views to all be positioned at the top when there is enough space, you should add these constraints relating the stack view and its super view:

NSLayoutConstraint.activate([
    stackView.topAnchor.constraint(equalTo: view.topAnchor),
    stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
    stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
    stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor),
])

Note especially the last one - it's another "<=" constraint. This is what prevents the stack view from exceeding the screen's frame in landscape.

Output:

enter image description here

enter image description here

Related