Подтвердить что ты не робот

Мимический подход UIStackView `fill пропорционально 'в версии iOS до 9.0

iOS 9.0 поставляется с UIStackView, что упрощает просмотр макетов по размеру их содержимого. Например, чтобы разместить 3 кнопки в строке в соответствии с их шириной содержимого, вы можете просто вставить их в представление стека, установить горизонтальную ось и распределение - заполнить пропорционально.

Решение для просмотра стека

Вопрос заключается в том, как добиться того же результата в более старых версиях iOS, где представление стека не поддерживается.

Одно из решений, которое я придумал, является грубым и выглядит не очень хорошо. Опять же, вы помещаете 3 кнопки подряд и привязываете их к ближайшим соседям с использованием ограничений. После этого вы, очевидно, увидите ошибку двусмысленности приоритета контента, потому что система автоматического макета не имеет понятия, какая кнопка должна расти/сокращаться перед другими.

Неопределенность приоритета контента

К сожалению, заголовки неизвестны до запуска приложения, поэтому вы можете произвольно выбрать кнопку. Скажем, я уменьшил горизонтальный контент, обнимающий приоритет средней кнопки от стандартных 250 до 249. Теперь он будет расти раньше других двух. Еще одна проблема заключается в том, что левая и правая кнопки строго сокращаются до их ширины содержимого без каких-либо красиво выглядящих paddings, как в версии Stack View.

Сопоставление компоновки

4b9b3361

Ответ 1

Да, мы можем получить те же результаты, используя только ограничения:)

исходный код

Представьте, у меня есть три метки:

  • firstLabel с размером собственного содержимого, равным (62,5, 40)
  • secondLabel с размером собственного содержимого, равным (170,5, 40)
  • thirdLabel с размером собственного содержимого, равным (54, 40)

Strucuture

-- ParentView --
    -- UIView -- (replace here the UIStackView)
       -- Label 1 -- 
       -- Label 2 -- 
       -- Label 3 --            

Ограничения

например, UIView имеет следующие ограничения: view.leading = superview.leading, view.trailing = superview.trailing, и он центрирован по вертикали

введите описание изображения здесь

Ограничения UILabels

введите описание изображения здесь

SecondLabel.width равно:

firstLabel.width * (secondLabelIntrinsicSizeWidth/firstLabelIntrinsicSizeWidth)

ThirdLabel.width равно:

firstLabel.width * (thirdLabelIntrinsicSizeWidth/firstLabelIntrinsicSizeWidth)

Я вернусь для более подробных объяснений

введите описание изображения здесь

Ответ 2

Кажется сложным для такой простой вещи. Но значение множителя ограничения доступно только для чтения, поэтому вам придется идти сложным путем.

Я сделал бы это так, если бы мне пришлось:

  • В IB: создайте UIView с ограничениями для заполнения по горизонтали супервизора (например)

  • В IB: добавьте свои 3 кнопки, добавьте противопоказания, чтобы выровнять их по горизонтали.

  • В коде: программно создать 1 NSConstraint между каждым UIButton и UIView с атрибутом NSLayoutAttributeWidth и множителем 0,33.

Здесь вы получите 3 кнопки с одинаковой шириной, используя 1/3 ширины UIView.

  1. Соблюдайте title ваших кнопок (используйте KVO или подкласс UIButton). Когда заголовок изменяется, подсчитайте размер содержимого вашей кнопки примерно следующим образом:

    CGSize stringsize = [myButton.title sizeWithAttributes: @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];

  2. Удалите все программно созданные ограничения.

  3. Сравните расчетную ширину (на шаге 4) каждой кнопки с шириной UIView и определите соотношение для каждой кнопки.

  4. Повторно создайте ограничения для шага 3 таким же образом, но заменив 0.33 на отношения, вычисленные на этапе 6, и добавьте их в элементы интерфейса.

Ответ 3

Возможно, вы захотите рассмотреть резерв UIStackView, существует несколько проектов с открытым исходным кодом. Преимущество здесь в том, что в конечном итоге, если вы перейдете к UIStackView, у вас будут минимальные изменения кода. Я использовал TZStackView, и он работал превосходно.

В качестве альтернативы, более легкое решение будет состоять в том, чтобы просто воспроизвести логику для пропорционального представления вида стека.

  • Рассчитать общую внутреннюю ширину содержимого представлений в вашем стеке
  • Установите ширину каждого представления, равную представлению родительского стека, умноженному на его долю от общей внутренней ширины содержимого.

    Я прикрепил приблизительный пример горизонтального представления пропорционального стека ниже, вы можете запустить его на Swift Playground.

import UIKit
import XCPlayground

let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))

view.layer.borderWidth = 1
view.layer.borderColor = UIColor.grayColor().CGColor
view.backgroundColor = UIColor.whiteColor()

XCPlaygroundPage.currentPage.liveView = view

class ProportionalStackView: UIView {

  private var stackViewConstraints = [NSLayoutConstraint]()

  var arrangedSubviews: [UIView] {
    didSet {
      addArrangedSubviews()
      setNeedsUpdateConstraints()
    }
  }

  init(arrangedSubviews: [UIView]) {
    self.arrangedSubviews = arrangedSubviews
    super.init(frame: CGRectZero)
    addArrangedSubviews()
  }

  convenience init() {
    self.init(arrangedSubviews: [])
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func updateConstraints() {
    removeConstraints(stackViewConstraints)
    var newConstraints = [NSLayoutConstraint]()

    for (n, subview) in arrangedSubviews.enumerate() {

      newConstraints += buildVerticalConstraintsForSubview(subview)

      if n == 0 {
        newConstraints += buildLeadingConstraintsForLeadingSubview(subview)
      } else {
        newConstraints += buildConstraintsBetweenSubviews(arrangedSubviews[n-1], subviewB: subview)
      }

      if n == arrangedSubviews.count - 1 {
        newConstraints += buildTrailingConstraintsForTrailingSubview(subview)
      }

    }

    // for proportional widths, need to determine contribution of each subview to total content width
    let totalIntrinsicWidth = subviews.reduce(0) { $0 + $1.intrinsicContentSize().width }

    for subview in arrangedSubviews {
      let percentIntrinsicWidth = subview.intrinsicContentSize().width / totalIntrinsicWidth
      newConstraints.append(NSLayoutConstraint(item: subview, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: percentIntrinsicWidth, constant: 0))
    }

    addConstraints(newConstraints)
    stackViewConstraints = newConstraints

    super.updateConstraints()
  }

}

// Helper methods
extension ProportionalStackView {

  private func addArrangedSubviews() {
    for subview in arrangedSubviews {
      if subview.superview != self {
        subview.removeFromSuperview()
        addSubview(subview)
      }
    }
  }

  private func buildVerticalConstraintsForSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

  private func buildLeadingConstraintsForLeadingSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("|-0-[subview]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

  private func buildConstraintsBetweenSubviews(subviewA: UIView, subviewB: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("[subviewA]-0-[subviewB]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subviewA": subviewA, "subviewB": subviewB])
  }

  private func buildTrailingConstraintsForTrailingSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

}

let labelA = UILabel()
labelA.text = "Foo"

let labelB = UILabel()
labelB.text = "FooBar"

let labelC = UILabel()
labelC.text = "FooBarBaz"

let stack = ProportionalStackView(arrangedSubviews: [labelA, labelB, labelC])

stack.translatesAutoresizingMaskIntoConstraints = false
labelA.translatesAutoresizingMaskIntoConstraints = false
labelB.translatesAutoresizingMaskIntoConstraints = false
labelC.translatesAutoresizingMaskIntoConstraints = false
labelA.backgroundColor = UIColor.orangeColor()
labelB.backgroundColor = UIColor.greenColor()
labelC.backgroundColor = UIColor.redColor()

view.addSubview(stack)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-0-[stack]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["stack": stack]))
view.addConstraint(NSLayoutConstraint(item: stack, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0))

Ответ 4

Используйте автозапуск в своих интересах. Он может сделать весь тяжелый подъем для вас.

Вот UIViewController, который выдает 3 UILabels, как у вас на снимке экрана, без вычислений. Есть 3 UIView subviews, которые используются, чтобы дать ярлыкам "padding" и установить цвет фона. Каждый из этих UIViews имеет подтекст UILabel, который просто показывает текст и ничего больше.

Вся компоновка выполняется с автозагрузкой в ​​ viewDidLoad, что означает отсутствие расчетных коэффициентов или кадров и отсутствие KVO. Изменение таких вещей, как прокладка и сжатие/обнимание приоритетов - это легкий ветерок. Это также потенциально позволяет избежать зависимости от решения с открытым исходным кодом, такого как TZStackView. Это так же легко настраивается в конструкторе интерфейса без необходимости использования кода.

class StackViewController: UIViewController {

    private let leftView: UIView = {
        let leftView = UIView()
        leftView.translatesAutoresizingMaskIntoConstraints = false
        leftView.backgroundColor = .blueColor()
        return leftView
    }()

    private let leftLabel: UILabel = {
        let leftLabel = UILabel()
        leftLabel.translatesAutoresizingMaskIntoConstraints = false
        leftLabel.textColor = .whiteColor()
        leftLabel.text = "A medium title"
        leftLabel.textAlignment = .Center
        return leftLabel
    }()

    private let middleView: UIView = {
        let middleView = UIView()
        middleView.translatesAutoresizingMaskIntoConstraints = false
        middleView.backgroundColor = .redColor()
        return middleView
    }()

    private let middleLabel: UILabel = {
        let middleLabel = UILabel()
        middleLabel.translatesAutoresizingMaskIntoConstraints = false
        middleLabel.textColor = .whiteColor()
        middleLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
        middleLabel.textAlignment = .Center
        return middleLabel
    }()

    private let rightView: UIView = {
        let rightView = UIView()
        rightView.translatesAutoresizingMaskIntoConstraints = false
        rightView.backgroundColor = .greenColor()
        return rightView
    }()

    private let rightLabel: UILabel = {
        let rightLabel = UILabel()
        rightLabel.translatesAutoresizingMaskIntoConstraints = false
        rightLabel.textColor = .whiteColor()
        rightLabel.text = "OK"
        rightLabel.textAlignment = .Center
        return rightLabel
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(leftView)
        view.addSubview(middleView)
        view.addSubview(rightView)

        leftView.addSubview(leftLabel)
        middleView.addSubview(middleLabel)
        rightView.addSubview(rightLabel)

        let views: [String : AnyObject] = [
            "topLayoutGuide" : topLayoutGuide,
            "leftView" : leftView,
            "leftLabel" : leftLabel,
            "middleView" : middleView,
            "middleLabel" : middleLabel,
            "rightView" : rightView,
            "rightLabel" : rightLabel
        ]

        // Horizontal padding for UILabels inside their respective UIViews
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[leftLabel]-(16)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[middleLabel]-(16)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[rightLabel]-(16)-|", options: [], metrics: nil, views: views))

        // Vertical padding for UILabels inside their respective UIViews
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[leftLabel]-(6)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[middleLabel]-(6)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[rightLabel]-(6)-|", options: [], metrics: nil, views: views))

        // Set the views' vertical position. The height can be determined from the label intrinsic content size, so you only need to specify a y position to layout from. In this case, we specified the top of the screen.
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][leftView]", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][middleView]", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][rightView]", options: [], metrics: nil, views: views))

        // Horizontal layout of views
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[leftView][middleView][rightView]|", options: [], metrics: nil, views: views))

        // Make sure the middle view is the view that expands to fill up the extra space
        middleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
        middleView.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
    }

}

Результат: