由于我最初的答案并不完全正确,因此需要进行澄清编辑...
首先,UIStackView 的 .fillProportionally 分布属性经常被误解。
其次,当堆栈视图的分布是.fillProportionally 并且堆栈视图的.spacing 不是0,或者当堆栈视图也设置了.layoutMargins。
您遇到的问题是自动布局计算比例大小的方式。
根据实验,自动布局计算视图的比例宽度,然后应用布局边距,减去容纳空间的最后一个视图。
这可以很容易地证明如下:
有 6 个水平堆栈视图,每个设置为 200 点宽,分布设置为 .fillProportionally,并填充一个或两个视图。红色视图的固有宽度为 25,绿色视图为 75。
第一个堆栈视图,具有单个视图且没有布局边距,按预期填充宽度...红色视图占用 100% 的空间。
第二个堆栈视图,有两个视图,没有布局边距,也按预期填充...红色视图为 50 磅宽 (25%),绿色视图为 150 磅宽 (75%)。
不过,第三个堆栈视图开始显示问题。单个视图的比例宽度为 100% 或 200-pts……但随后会应用布局边距。这会将视图从左侧移动 10 分,但因为自动布局不会从 first 子视图中减去空间,它实际上会延伸 10 分到堆栈视图的边缘(所以红色视图仍然 200 分宽)。
第四个堆栈视图看起来就像它正在做我们想要的...比例填充,每边有 10 分的边距...红色视图是 50 分宽(200 的 25% ) 但是绿色视图只有 130 点宽。所以自动布局给了两个视图 50 分 (25%) 和 150 分 (75%) 但 然后它应用了边距 并拿走了 20 分从绿色视图。
为底部的两个堆栈视图使用left: 100 right: 0 或left: 0 right: 100 的布局边距使其更加明显。同样,对于其中的每一个,Red 获得 50 分 (25%),Green 获得 150 分 (75%),但随后 Green 的 100 分优势被剥夺。
所以,要回答最初的问题,即为什么当我们有一个排列的子视图和布局边距时我们会得到模棱两可的约束,请查看堆栈视图 3。自动布局无法为 Red 提供 100% 的空间并应用边距,因此会引发布局错误。
这是运行上述示例的代码。如果您注释掉第三个堆栈视图,您不会收到错误:
class ProportionalStackExampleViewController: UIViewController {
let outerStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
return v
}()
let outerStackFrame: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.layer.borderWidth = 0.5
v.layer.borderColor = UIColor.blue.cgColor
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
v.numberOfLines = 0
v.textAlignment = .center
v.text = "Red views have intrinsic width of 25\nGreen views have intrinsic width of 75\nAll horizontal stack views are 200-pts wide\nTap any view to see its width"
return v
}()
let sizeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "(width)"
return v
}()
let myGreen = UIColor(red: 0, green: 0.75, blue: 0, alpha: 1.0)
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...6 {
let lbl = UILabel()
lbl.font = UIFont.systemFont(ofSize: 12.0, weight: .light)
lbl.numberOfLines = 0
lbl.textAlignment = .center
outerStackView.addArrangedSubview(lbl)
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.axis = .horizontal
sv.distribution = .fillProportionally
sv.spacing = 0
outerStackView.addArrangedSubview(sv)
}
view.addSubview(infoLabel)
view.addSubview(sizeLabel)
view.addSubview(outerStackFrame)
view.addSubview(outerStackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
sizeLabel.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
sizeLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
outerStackView.topAnchor.constraint(equalTo: sizeLabel.bottomAnchor, constant: 20.0),
outerStackView.widthAnchor.constraint(equalToConstant: 200.0),
outerStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
outerStackFrame.widthAnchor.constraint(equalTo: outerStackView.widthAnchor),
outerStackFrame.heightAnchor.constraint(equalTo: outerStackView.heightAnchor),
outerStackFrame.centerXAnchor.constraint(equalTo: outerStackView.centerXAnchor),
outerStackFrame.centerYAnchor.constraint(equalTo: outerStackView.centerYAnchor),
])
// StackView 1
if let lbl = outerStackView.arrangedSubviews[0] as? UILabel,
let sv = outerStackView.arrangedSubviews[1] as? UIStackView {
lbl.text = "One view, no layoutMargins"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
// StackView 2
if let lbl = outerStackView.arrangedSubviews[2] as? UILabel,
let sv = outerStackView.arrangedSubviews[3] as? UIStackView {
lbl.text = "Two views, no layoutMargins"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)
tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
// comment out this block to see the auto-layout error goes away
// StackView 3
if let lbl = outerStackView.arrangedSubviews[4] as? UILabel,
let sv = outerStackView.arrangedSubviews[5] as? UIStackView {
lbl.text = "One view\nlayoutMargins left: 10 right: 10"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
// StackView 4
if let lbl = outerStackView.arrangedSubviews[6] as? UILabel,
let sv = outerStackView.arrangedSubviews[7] as? UIStackView {
lbl.text = "Two views\nlayoutMargins left: 10 right: 10"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)
sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 10, bottom: 0, right: 10)
tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
// StackView 5
if let lbl = outerStackView.arrangedSubviews[8] as? UILabel,
let sv = outerStackView.arrangedSubviews[9] as? UIStackView {
lbl.text = "layoutMargins left: 100 right: 0"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)
sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 100, bottom: 0, right: 0)
tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
// StackView 6
if let lbl = outerStackView.arrangedSubviews[10] as? UILabel,
let sv = outerStackView.arrangedSubviews[11] as? UIStackView {
lbl.text = "layoutMargins left: 0 right: 100"
var v = ProportionalView()
v.w = 25.0
v.backgroundColor = .red
sv.addArrangedSubview(v)
var tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
v = ProportionalView()
v.w = 75.0
v.backgroundColor = myGreen
sv.addArrangedSubview(v)
sv.isLayoutMarginsRelativeArrangement = true
sv.layoutMargins = .init(top: 0, left: 0, bottom: 0, right: 100)
tg = UITapGestureRecognizer(target: self, action: #selector(showWidth(_:)))
v.addGestureRecognizer(tg)
}
}
@objc func showWidth(_ sender: UITapGestureRecognizer) -> Void {
if let v = sender.view {
sizeLabel.text = "Width: \(v.frame.width)"
sizeLabel.textColor = v.backgroundColor
}
}
}
class ProportionalView: UIView {
var w: CGFloat = 1.0
override var intrinsicContentSize: CGSize {
return CGSize(width: w, height: 40.0)
}
}