Мимический подход UIStackView `fill пропорционально 'в версии iOS до 9.0
iOS 9.0 поставляется с UIStackView, что упрощает просмотр макетов по размеру их содержимого. Например, чтобы разместить 3 кнопки в строке в соответствии с их шириной содержимого, вы можете просто вставить их в представление стека, установить горизонтальную ось и распределение - заполнить пропорционально.
![Решение для просмотра стека]()
Вопрос заключается в том, как добиться того же результата в более старых версиях iOS, где представление стека не поддерживается.
Одно из решений, которое я придумал, является грубым и выглядит не очень хорошо. Опять же, вы помещаете 3 кнопки подряд и привязываете их к ближайшим соседям с использованием ограничений. После этого вы, очевидно, увидите ошибку двусмысленности приоритета контента, потому что система автоматического макета не имеет понятия, какая кнопка должна расти/сокращаться перед другими.
![Неопределенность приоритета контента]()
К сожалению, заголовки неизвестны до запуска приложения, поэтому вы можете произвольно выбрать кнопку. Скажем, я уменьшил горизонтальный контент, обнимающий приоритет средней кнопки от стандартных 250 до 249. Теперь он будет расти раньше других двух. Еще одна проблема заключается в том, что левая и правая кнопки строго сокращаются до их ширины содержимого без каких-либо красиво выглядящих paddings, как в версии Stack View.
![Сопоставление компоновки]()
Ответы
Ответ 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.
-
Соблюдайте title
ваших кнопок (используйте KVO или подкласс UIButton).
Когда заголовок изменяется, подсчитайте размер содержимого вашей кнопки примерно следующим образом:
CGSize stringsize = [myButton.title sizeWithAttributes:
@{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];
-
Удалите все программно созданные ограничения.
-
Сравните расчетную ширину (на шаге 4) каждой кнопки с шириной UIView и определите соотношение для каждой кнопки.
-
Повторно создайте ограничения для шага 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)
}
}
Результат:
![]()