这里有几个选项:
-
Set .contentMode = .top
在标签上。我从未找到过清楚描述使用的 Apple 文档.contentMode
with UILabel
,但它有效并且should work.
-
将标签嵌入到UIView
,限制在顶部,其中Content Compression Resistance Priority
set to .required
,低于底部约束所需的优先级,以及.clipsToBounds = true
在视图上。
示例 1 - 内容模式:
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let botLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// let's add a label and a Switch to toggle the labels .contentMode
let promptView = UIView()
let hStack = UIStackView()
hStack.spacing = 8
let prompt = UILabel()
prompt.text = "Content Mode Top:"
prompt.textAlignment = .right
let sw = UISwitch()
sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
hStack.addArrangedSubview(prompt)
hStack.addArrangedSubview(sw)
hStack.translatesAutoresizingMaskIntoConstraints = false
promptView.addSubview(hStack)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[promptView, stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
promptView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.topAnchor.constraint(equalTo: promptView.bottomAnchor, constant: 0.0),
// center the hStack in the promptView
hStack.centerXAnchor.constraint(equalTo: promptView.centerXAnchor),
hStack.centerYAnchor.constraint(equalTo: promptView.centerYAnchor),
promptView.heightAnchor.constraint(equalTo: hStack.heightAnchor, constant: 16.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
@objc func switchChanged(_ sender: UISwitch) {
[topLabel, botLabel].forEach { v in
v.contentMode = sender.isOn ? .top : .left
}
}
@objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
示例 2 - 嵌入在 a 中的标签UIView
:
class TopAlignedLabelView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.addSubview(label)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
// we need bottom anchor to have
// less-than-required Priority
let c = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
c.priority = .required - 1
c.isActive = true
// don't allow label to be compressed
label.setContentCompressionResistancePriority(.required, for: .vertical)
// we need to clip the label
self.clipsToBounds = true
}
}
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let botLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
if let vv = v as? UILabel {
vv.font = .systemFont(ofSize: 24.0, weight: .light)
}
if let vv = v as? TopAlignedLabelView {
vv.label.font = .systemFont(ofSize: 24.0, weight: .light)
}
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.label.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.label.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
@objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
Edit
如果您的目标是让棕色标签“向上滑动并覆盖”both蓝色和粉色标签,这两个标签都没有压缩或移动,采用了类似的方法:
- 使用标准 UILabel 而不是 TopAlignedLabelView
- 将蓝色和粉色标签嵌入到它们自己的堆栈视图中
- embed that“容器”视图中的堆栈视图
- 约束that堆栈视图“顶部对齐”,就像我们对 TopAlignedLabelView 中的标签所做的那样
“外部”堆栈视图的排列子视图现在将是:
为了制作动画,我们将切换.alpha
and .isHidden
在“容器”视图上而不是蓝色和粉色标签上。
我编辑了控制器类——尝试一下,看看这是否是您想要的效果。
如果是,我强烈建议您尝试自己进行这些更改...如果您遇到问题,请使用以下示例代码作为指导:
class StackAnimVC: UIViewController {
let outerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
// create an "inner" stack view
// this will hold topLabel and botLabel
let innerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
return v
}()
// container for the inner stack view
let innerStackContainer: UIView = {
let v = UIView()
v.clipsToBounds = true
return v
}()
// we can use standard UILabels instead of custom views
let topLabel = UILabel()
let botLabel = UILabel()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to inner stack view
innerStackView.addArrangedSubview(topLabel)
innerStackView.addArrangedSubview(botLabel)
// add inner stack view to container
innerStackView.translatesAutoresizingMaskIntoConstraints = false
innerStackContainer.addSubview(innerStackView)
// constraints for inner stack view
// bottom constraint must be less-than-required
// so it doesn't compress when the container compresses
let isvBottom: NSLayoutConstraint = innerStackView.bottomAnchor.constraint(equalTo: innerStackContainer.bottomAnchor, constant: -8.0)
isvBottom.priority = .defaultHigh
NSLayoutConstraint.activate([
innerStackView.topAnchor.constraint(equalTo: innerStackContainer.topAnchor, constant: 0.0),
innerStackView.leadingAnchor.constraint(equalTo: innerStackContainer.leadingAnchor, constant: 0.0),
innerStackView.trailingAnchor.constraint(equalTo: innerStackContainer.trailingAnchor, constant: 0.0),
isvBottom,
])
topLabel.numberOfLines = 0
botLabel.numberOfLines = 0
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
// add views to outer stack view
[headerLabel, innerStackContainer, threeLabel, footerLabel].forEach { v in
outerStackView.addArrangedSubview(v)
}
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[outerStackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
outerStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
@objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on inner stack container
self.innerStackContainer.alpha = self.innerStackContainer.isHidden ? 1.0 : 0.0
self.innerStackContainer.isHidden.toggle()
}
}
}
Edit 2
快速解释为什么它有效......
考虑一个典型的UILabel
作为 a 的子视图UIView
。我们用一点“填充”将标签限制在所有 4 个侧面的视图上:
aLabel.topAnchor.constraint(equalTo: aView.topAnchor, constant: 8.0),
aLabel.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 8.0),
aLabel.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: -8.0),
aLabel.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: -8.0),
现在我们可以约束视图的顶部/前导/尾随——但不能约束底部或高度——并且标签的固有高度将控制视图的高度。
非常基本。
但是,如果我们想“让它消失”,改变视图的高度将also更改标签的高度,从而产生“挤压”效果。我们还会收到自动布局投诉,因为无法满足约束条件。
所以,我们需要改变.priority
标签的底部约束,以允许其保持其固有高度,而其超级视图的高度发生变化。
这 4 个示例中的每一个都使用相同的顶部/前导/尾随约束...唯一的区别是我们对底部约束的处理方式:
For 实施例1,我们不设置any底部约束。因此,我们甚至从未看到它的超级视图,并且对其超级视图的高度进行动画处理对标签没有影响。
For 实施例2,我们设置“正常”底部约束,我们看到“挤压”效果。
For 实施例3,我们给出标签的底部约束.priority = .defaultHigh
。标签仍然控制其超级视图的高度...直到我们激活超级视图的高度约束(为零)。超级视图折叠,但我们已经授予自动布局权限来打破底部约束。
实施例4是相同的3,但我们也设置了.clipsToBounds = true
在容器视图上,因此标签高度保持不变,但不再延伸到其超级视图之外。
设置时所有这些也适用于堆栈视图中的视图.isHidden
在安排的子视图上。
如果您想检查它并尝试各种变化,这里是生成该示例的代码:
class DemoVC: UIViewController {
var containerViews: [UIView] = []
var heightConstraints: [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
// create 4 container views, each with a label as a subview
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .systemYellow,
]
colors.forEach { bkgColor in
let thisContainer = UIView()
thisContainer.translatesAutoresizingMaskIntoConstraints = false
let thisLabel = UILabel()
thisLabel.translatesAutoresizingMaskIntoConstraints = false
thisContainer.backgroundColor = bkgColor
thisLabel.backgroundColor = UIColor(red: 0.75, green: 0.9, blue: 1.0, alpha: 1.0)
thisLabel.numberOfLines = 0
//thisLabel.font = .systemFont(ofSize: 20.0, weight: .light)
thisLabel.font = .systemFont(ofSize: 12.0, weight: .light)
thisLabel.text = "We want to animate compressing the \"container\" view vertically, without it squeezing or moving this label."
// add label to container view
thisContainer.addSubview(thisLabel)
// add container view to array
containerViews.append(thisContainer)
// add container view to view
view.addSubview(thisContainer)
NSLayoutConstraint.activate([
// each example gets the label constrained
// Top / Leading / Trailing to its container view
thisLabel.topAnchor.constraint(equalTo: thisContainer.topAnchor, constant: 8.0),
thisLabel.leadingAnchor.constraint(equalTo: thisContainer.leadingAnchor, constant: 8.0),
thisLabel.trailingAnchor.constraint(equalTo: thisContainer.trailingAnchor, constant: -8.0),
// we'll be using different bottom constraints for the examples,
// so don't set it here
//thisLabel.bottomAnchor.constraint(equalTo: thisContainer.bottomAnchor, constant: -8.0),
// each container view gets constrained to the top
thisContainer.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),
])
// setup the container view height constraints, but don't activate them
let hc = thisContainer.heightAnchor.constraint(equalToConstant: 0.0)
// add the constraint to the constraints array
heightConstraints.append(hc)
}
// couple vars to reuse
var prevContainer: UIView!
var aContainer: UIView!
var itsLabel: UIView!
var bc: NSLayoutConstraint!
// -------------------------------------------------------------------
// first example
// we don't add a bottom constraint for the label
// that means we'll never see its container view
// and changing its height constraint won't do anything to the label
// -------------------------------------------------------------------
// second example
aContainer = containerViews[1]
itsLabel = aContainer.subviews.first
// we'll add a "standard" bottom constraint
// so now we see its container view
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.isActive = true
// -------------------------------------------------------------------
// third example
aContainer = containerViews[2]
itsLabel = aContainer.subviews.first
// add the same bottom constraint, but give it a
// less-than-required Priority so it won't "squeeze"
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// -------------------------------------------------------------------
// fourth example
aContainer = containerViews[3]
itsLabel = aContainer.subviews.first
// same less-than-required Priority bottom constraint,
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// we'll also set clipsToBounds on the container view
// so it will "hide / reveal" the label
aContainer.clipsToBounds = true
// now we need to layout the views
// constrain first example leading
aContainer = containerViews[0]
aContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0).isActive = true
prevContainer = aContainer
for i in 1..<containerViews.count {
aContainer = containerViews[i]
aContainer.leadingAnchor.constraint(equalTo: prevContainer.trailingAnchor, constant: 8.0).isActive = true
aContainer.widthAnchor.constraint(equalTo: prevContainer.widthAnchor).isActive = true
prevContainer = aContainer
}
// constrain last example trailing
prevContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0).isActive = true
// and, let's add labels above the 4 examples
for (i, v) in containerViews.enumerated() {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Example \(i + 1)"
label.font = .systemFont(ofSize: 14.0, weight: .light)
view.addSubview(label)
NSLayoutConstraint.activate([
label.bottomAnchor.constraint(equalTo: v.topAnchor, constant: -4.0),
label.centerXAnchor.constraint(equalTo: v.centerXAnchor),
])
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
heightConstraints.forEach { c in
c.isActive = !c.isActive
}
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
}
}