С программными ограничениями swift ios, как указать вид, который по умолчанию равен 50% высоты, но при необходимости может уменьшаться?

#ios #swift #autolayout

#iOS #swift #автозаполнение

Вопрос:

Я использую NSLayoutConstraint для ограничения представления. Я хочу, чтобы его высота по умолчанию занимала 50% экрана, но если для других компонентов недостаточно места (например, iphone в альбомной ориентации), вид может уменьшиться до 10% высоты.

Я пытаюсь:

        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)
  

К сожалению, это отображает только 10% высоты экрана.

Меня смущают две вещи:

  1. Когда я устанавливаю неоднозначные ограничения, подобные этому, которые в основном говорят «от 10% до 50%», как он определяет, какую высоту ему дать? По умолчанию используется минимальный объем пространства?

  2. Я думал, что ограничения должны иметь только одно решение. Почему я не получаю ошибку неоднозначности, поскольку любые высоты от 10% до 50% были бы допустимыми решениями здесь?

Наконец, как мне получить то, что я хочу, представление на 50%, которое при необходимости может уменьшаться?

Большое спасибо!

Комментарии:

1. «Почему я не получаю ошибку неоднозначности» Такой вещи, как ошибка неоднозначности, не существует.

2. @matt Ах, я прочитал это на сайте разработчика Apple «При использовании автоматической компоновки цель состоит в том, чтобы предоставить ряд уравнений, которые имеют одно и только одно возможное решение. Неоднозначные ограничения имеют более одного возможного решения. У неудовлетворительных ограничений нет допустимых решений «. и предполагаемая двусмысленность означала ошибку, но я предполагаю, что они просто означают, что это нежелательно в качестве макета, потому что оно неоднозначно.

3. Ну, подумайте об этом так. Если вы добавляете ограничения по одному, они неоднозначны после каждого оператора. Вы бы не хотели получать сообщение об ошибке только потому, что вы еще не закончили. Один ботинок — это не ошибка; вы просто не полностью одеты. Это плохо, но это может быть временным. Обе туфли на неправильных ногах — ошибка. 🙂

Ответ №1:

Вы можете сделать это, изменив Priority ограничение высоты 50%.

Мы сообщим автоматической компоновке, что кнопка должна составлять не менее 10% от высоты представления.

И мы сообщим auto-layout, что мы хотим, чтобы кнопка составляла 50% от высоты представления, но:

 .priority = .defaultHigh
  

в котором говорится: «вы можете нарушить это ограничение, если это необходимо».

Итак…

     // 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
    }
    
  

Или с более современным синтаксисом…

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

Теперь, какие бы другие элементы пользовательского интерфейса у вас ни были, которые уменьшат доступное пространство, авто-макет установит высоту кнопки на «как можно ближе к 50%, но всегда не менее 10%»

Вот полный пример для демонстрации. Я использую две метки (синяя сверху как «кнопка» и красная снизу). Нажатие увеличит высоту красной метки, пока она не начнет «поднимать нижнюю часть» или «сжимать» синюю метку:

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