简体   繁体   中英

With swift ios programming constraints, how to specify a view that defaults to 50% of the height but can shrink if needed?

I'm using NSLayoutConstraint to constrain a view. I want its height to take up 50% of the screen by default, but if there is not enough room for the other components (eg iphone in landscape), the view can shrink to as little as 10% of the height.

I'm trying:

       let y1 = NSLayoutConstraint(item: button, attribute: .top, relatedBy: .equal,
toItem: self.view, attribute: .top, multiplier: 1, constant: 0)

        let y2 = NSLayoutConstraint(item: button, attribute: .height, relatedBy: .lessThanOrEqual, 
toItem: self.view, attribute: .height, multiplier: 0.5, constant: 0)
        
        let y3 = NSLayoutConstraint(item: button, attribute: .height, relatedBy: .greaterThanOrEqual, 
toItem: self.view, attribute: .height, multiplier: 0.1, constant: 0)

Unfortunately this renders as only 10% of the height of the screen.

I'm confused by two things:

  1. When I set ambiguous constraints like this, that basically say "between 10% and 50%", how does it decide how much height to give it? Does it default to the minimum amount of space?

  2. I thought that constraints had to only have one solution. Why don't I get an ambiguity error, since any heights from 10% to 50% would be valid solutions here?

Finally, how do I get what I want, a 50% view that can shrink if needed?

Many thanks!

You can do this by changing the Priority of the 50% height constraint.

We'll tell auto-layout the button must be at least 10% of the height of the view.

And, we'll tell auto-layout we want the button to be 50% of the height of the view, but:

.priority = .defaultHigh

which says "you can break this constraint if needed."

So...

    // constrain button Top to view Top
    let btnTop = NSLayoutConstraint(item: button, attribute: .top, relatedBy: .equal,
                                toItem: self.view, attribute: .top, multiplier: 1, constant: 0)

    // button Height Greater Than Or Equal To 10%
    let percent10 = NSLayoutConstraint(item: button, attribute: .height, relatedBy: .greaterThanOrEqual,
                                toItem: self.view, attribute: .height, multiplier: 0.1, constant: 0)

    // button Height Equal To 50%
    let percent50 = NSLayoutConstraint(item: button, attribute: .height, relatedBy: .equal,
                                toItem: self.view, attribute: .height, multiplier: 0.5, constant: 0)

    // let auto-layout break the 50% height constraint if necessary
    percent50.priority = .defaultHigh

    [btnTop, percent10, percent50].forEach {
        $0.isActive = true
    }
    

Or, with more modern syntax...

    let btnTop = button.topAnchor.constraint(equalTo: view.topAnchor)
    let percent10 = button.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor, multiplier: 0.10)
    let percent50 = button.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.50)
    percent50.priority = .defaultHigh
    
    NSLayoutConstraint.activate([btnTop, percent10, percent50])

Now, whatever other UI elements you have that will reduce the available space, auto-layout will set the button's height to "as close to 50% as possible, but always at least 10%"

Here's a complete example to demonstrate. I'm using two labels (blue on top as the "button" and red on the bottom). Tapping will increase the height of the red label, until it starts to "push up the bottom" or "compress" the blue label:

class ExampleViewController: UIViewController {
    
    let blueLabel = UILabel()
    let redLabel = UILabel()
    
    var viewSafeAreaHeight: CGFloat = 0
    
    var adjustableLabelHeightConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        [blueLabel, redLabel].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            v.textAlignment = .center
            v.textColor = .white
            v.numberOfLines = 0
        }
        blueLabel.backgroundColor = .blue
        redLabel.backgroundColor = .red
        
        view.addSubview(blueLabel)
        view.addSubview(redLabel)
        
        // blueLabel should be 50% of the height if possible
        //  otherwise, let it shrink to minimum of 10%
        
        // so, we'll constrain redLabel to the bottom of the view
        //  and give it a Height constraint that we can change
        //  so it can "compress" blueLabel
        
        // we'll constrain the bottom of blueLabel to stay above the top of redLabel
        
        // let's respect the safe-area
        let safeArea = view.safeAreaLayoutGuide
        
        // start by horizontally centering both elements,
        //  and 75% of the width of the view
        
        blueLabel.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor).isActive = true
        redLabel.centerXAnchor.constraint(equalTo: safeArea.centerXAnchor).isActive = true
        
        blueLabel.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.75).isActive = true
        redLabel.widthAnchor.constraint(equalTo: safeArea.widthAnchor, multiplier: 0.75).isActive = true
        
        // now, let's constrain redLabel to the bottom
        redLabel.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
        
        // tell the Bottom of blueLabel to stay Above the top of redLabel
        blueLabel.bottomAnchor.constraint(lessThanOrEqualTo: redLabel.topAnchor, constant: 0.0).isActive = true
        
        // next, constrain the top of blueLabel to the top
        let blueLabelTop = blueLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 0.0)
        
        // blueLabel height must be At Least 10% of the view
        let blue10 = blueLabel.heightAnchor.constraint(greaterThanOrEqualTo: safeArea.heightAnchor, multiplier: 0.10)
        
        // blueLabel should be 50% if possible -- so we'll set the
        //  Priority on that constraint to less than Required
        let blue50 = blueLabel.heightAnchor.constraint(equalTo: safeArea.heightAnchor, multiplier: 0.50)
        blue50.priority = .defaultHigh

        // start redLabel Height at 100-pts
        adjustableLabelHeightConstraint = redLabel.heightAnchor.constraint(equalToConstant: 100.0)
        // we'll be increasing the Height constant past the available area,
        //  so we also need to change its Priority so we don't get
        //  auto-layout conflict errors
        // and, we need to set it GREATER THAN blueLabel's height priority
        adjustableLabelHeightConstraint.priority = UILayoutPriority(rawValue: blue50.priority.rawValue + 1)
        
        // activate those constraints
        NSLayoutConstraint.activate([blueLabelTop, blue10, blue50, adjustableLabelHeightConstraint])

        // add a tap gesture recognizer so we can increas the height of the label
        let t = UITapGestureRecognizer(target: self, action: #selector(self.gotTap(_:)))
        view.addGestureRecognizer(t)
        
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        viewSafeAreaHeight = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom)
        updateLabelText()
    }
    
    @objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
        adjustableLabelHeightConstraint.constant += 50
        updateLabelText()
    }
    
    func updateLabelText() -> Void {
        let blueHeight = blueLabel.frame.height
        let redHeight = redLabel.frame.height
        let redConstant = adjustableLabelHeightConstraint.constant
        
        let percentFormatter            = NumberFormatter()
        percentFormatter.numberStyle    = .percent
        percentFormatter.minimumFractionDigits = 2
        percentFormatter.maximumFractionDigits = 2
        
        guard let bluePct = percentFormatter.string(for: blueHeight / viewSafeAreaHeight) else { return }
        
        var s = "SafeArea Height: \(viewSafeAreaHeight)"
        s += "\n"
        s += "Blue Height: \(blueHeight)"
        s += "\n"
        s += "\(blueHeight) / \(viewSafeAreaHeight) = \(bluePct)"
        blueLabel.text = s
        
        s = "Tap to increase..."
        s += "\n"
        s += "Red Height Constant: \(redConstant)"
        s += "\n"
        s += "Red Actual Height: \(redHeight)"
        redLabel.text = s
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM