如何确保 UIStackView 在排列视图大小更改时通知其所有后代?
How can I make sure UIStackView notifies ALL it's descendants when it's arranged view's size changes?
我有一个 UIStackView
,其中有 2 个 child UIView。第二个 UIView(一个标题视图)将 contentHugging 设置为 high
,当它的内容发生变化时,第一个 UIView(一个内容视图)会正确地拉伸它的大小。一切看起来都很好。
问题在于,当 UIStackView 响应第二个视图更改大小而布置其视图时,第一个视图的 none 个 child 视图和后代会收到通知。
我认为viewDidLayoutSubviews
应该向下传播,所以我添加了
override func viewDidLayoutSubviews() {
let parentView = self.superview!
// This is always .zero
print(parentView.frame)
super.viewDidLayoutSubviews()
// This is ALSO always .zero
print(parentView.frame)
}
尽可能多的子视图,但它似乎从未报告任何视图更改超过一次,所以这个理论成立。
由于我们无法观察 UIView
帧值,确保后代视图始终知道内容视图的帧大小并可以相对于它调整自身大小的正确方法是什么?
这是一个完整的示例,显示了在堆栈视图的所有 arrangedSubviews
上调用了 layoutSubviews()
...and 他们所有的子视图。
这是开始的样子:
- 堆栈视图有一个 "dashed outline"
- "bottom"(绿色)视图是我们的 "Title" 视图 -
UIView
中的一个多行标签
- "top"(黄色)视图是我们的 "Content" 视图 - 两个标签和一个 "round" 视图
每次我们点击按钮,标题视图中的文本都会改变:
"round view" 保持圆形的事实告诉我们它的 layoutSubviews()
(我们更新 cornerRadius 的地方)正在被调用。如果不是,它将如下所示:
这是此示例的代码(无 @IBOutlet
或 @IBAction
连接):
class StackExampleViewController: UIViewController {
let testButton: UIButton = {
let v = UIButton()
v.setTitle("Tap Me", for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
let contentView: ContentView = {
let v = ContentView()
return v
}()
let titleView: TitleView = {
let v = TitleView()
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let dashedView: DashedBorderView = {
let v = DashedBorderView()
return v
}()
let sampleData: [String] = [
"This is the Title View",
"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set.",
"You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.",
"What's a UIButton?",
"You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."
]
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// standard auto-layout
[testButton, contentView, titleView, stackView, dashedView].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and titleView to stackView
stackView.addArrangedSubview(contentView)
stackView.addArrangedSubview(titleView)
// add button, dashedView and stackView
// (dashedView will be used to show the frame of stackView)
view.addSubview(testButton)
view.addSubview(dashedView)
view.addSubview(stackView)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// testButton 40-pts from top, centeredX
testButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// stackView 40-pts from testButton
// 40-pts on each side
stackView.topAnchor.constraint(equalTo: testButton.bottomAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// stackView height = 300
stackView.heightAnchor.constraint(equalToConstant: 300.0),
// constrain dashedView centered to stackView
// width and height 2-pts greater (so we can "outline" the stackView frame)
dashedView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: 2),
dashedView.heightAnchor.constraint(equalTo: stackView.heightAnchor, constant: 2),
dashedView.centerXAnchor.constraint(equalTo: stackView.centerXAnchor),
dashedView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
])
// add touchUp target
testButton.addTarget(self, action: #selector(self.didTap(_:)), for: .touchUpInside)
// this will track the text for updating titleView's label
idx = sampleData.count
updateText()
}
func updateText() -> Void {
// change the text in titleView's titleLabel
// let auto-layout handle ALL of the resizing
titleView.titleLabel.text = sampleData[idx % sampleData.count]
idx += 1
}
@objc func didTap(_ sender: Any) {
updateText()
}
}
class TitleView: UIView {
// TitleView has a multi-line UILabel
// with 20-pts "padding" on each side
// and 12-pts "padding" on top and bottom
var titleLabel: UILabel = {
let v = UILabel()
v.text = "Title Label"
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .green
[titleLabel].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
[=10=].backgroundColor = UIColor(white: 0.9, alpha: 1.0)
[=10=].textAlignment = .center
addSubview([=10=])
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
])
// we want this label's text to control its height
titleLabel.setContentHuggingPriority(.required, for: .vertical)
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class ContentView: UIView {
// ContentView has two labels and a "round" view
var labelA: UILabel = {
let v = UILabel()
v.text = "Label A"
return v
}()
var labelB: UILabel = {
let v = UILabel()
v.text = "The Content View is Yellow"
return v
}()
var roundView: RoundView = {
let v = RoundView()
v.backgroundColor = .orange
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .yellow
[labelA, labelB].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
[=10=].backgroundColor = .cyan
[=10=].textAlignment = .center
addSubview([=10=])
}
roundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(roundView)
NSLayoutConstraint.activate([
// constrain labelA 20-pts from top / leading
labelA.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
labelA.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// constrain roundView 20-pts from trailing
roundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// constrain labelA trailing 20-pts from roundView leading
labelA.trailingAnchor.constraint(equalTo: roundView.leadingAnchor, constant: -20.0),
// constrain roundView height equal to lableA height
roundView.heightAnchor.constraint(equalTo: labelA.heightAnchor),
// keep roundView square (1:1 ratio)
roundView.widthAnchor.constraint(equalTo: roundView.heightAnchor),
// center roundView vertically to labelA
roundView.centerYAnchor.constraint(equalTo: labelA.centerYAnchor),
// labelB top is 8-pts below labelA
labelB.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
// constrain labelB 20-pts from leading / trailing / bottom
labelB.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
labelB.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// keep labelA and labelB heights equal
labelA.heightAnchor.constraint(equalTo: labelB.heightAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class RoundView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
// update cornerRadius here to keep it round
layer.cornerRadius = bounds.size.width * 0.5
}
}
class DashedBorderView: UIView {
// simple view with dashed border
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor(red: 1.0, green: 0.25, blue: 0.25, alpha: 1.0).cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
注意:当视图收到对 layoutSubviews()
的调用时,它们也会打印 () 它们的 class 名称和边界,因此您会在调试控制台中看到类似这样的内容:
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 49.0, 49.0)
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 105.5)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 186.5)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 69.0, 69.5)
我有一个 UIStackView
,其中有 2 个 child UIView。第二个 UIView(一个标题视图)将 contentHugging 设置为 high
,当它的内容发生变化时,第一个 UIView(一个内容视图)会正确地拉伸它的大小。一切看起来都很好。
问题在于,当 UIStackView 响应第二个视图更改大小而布置其视图时,第一个视图的 none 个 child 视图和后代会收到通知。
我认为viewDidLayoutSubviews
应该向下传播,所以我添加了
override func viewDidLayoutSubviews() {
let parentView = self.superview!
// This is always .zero
print(parentView.frame)
super.viewDidLayoutSubviews()
// This is ALSO always .zero
print(parentView.frame)
}
尽可能多的子视图,但它似乎从未报告任何视图更改超过一次,所以这个理论成立。
由于我们无法观察 UIView
帧值,确保后代视图始终知道内容视图的帧大小并可以相对于它调整自身大小的正确方法是什么?
这是一个完整的示例,显示了在堆栈视图的所有 arrangedSubviews
上调用了 layoutSubviews()
...and 他们所有的子视图。
这是开始的样子:
- 堆栈视图有一个 "dashed outline"
- "bottom"(绿色)视图是我们的 "Title" 视图 -
UIView
中的一个多行标签
- "top"(黄色)视图是我们的 "Content" 视图 - 两个标签和一个 "round" 视图
每次我们点击按钮,标题视图中的文本都会改变:
"round view" 保持圆形的事实告诉我们它的 layoutSubviews()
(我们更新 cornerRadius 的地方)正在被调用。如果不是,它将如下所示:
这是此示例的代码(无 @IBOutlet
或 @IBAction
连接):
class StackExampleViewController: UIViewController {
let testButton: UIButton = {
let v = UIButton()
v.setTitle("Tap Me", for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
let contentView: ContentView = {
let v = ContentView()
return v
}()
let titleView: TitleView = {
let v = TitleView()
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
let dashedView: DashedBorderView = {
let v = DashedBorderView()
return v
}()
let sampleData: [String] = [
"This is the Title View",
"A label can contain an arbitrary amount of text, but UILabel may shrink, wrap, or truncate the text, depending on the size of the bounding rectangle and properties you set.",
"You can control the font, text color, alignment, highlighting, and shadowing of the text in the label.",
"What's a UIButton?",
"You can set the title, image, and other appearance properties of a button. In addition, you can specify a different appearance for each button state."
]
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// standard auto-layout
[testButton, contentView, titleView, stackView, dashedView].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and titleView to stackView
stackView.addArrangedSubview(contentView)
stackView.addArrangedSubview(titleView)
// add button, dashedView and stackView
// (dashedView will be used to show the frame of stackView)
view.addSubview(testButton)
view.addSubview(dashedView)
view.addSubview(stackView)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// testButton 40-pts from top, centeredX
testButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// stackView 40-pts from testButton
// 40-pts on each side
stackView.topAnchor.constraint(equalTo: testButton.bottomAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// stackView height = 300
stackView.heightAnchor.constraint(equalToConstant: 300.0),
// constrain dashedView centered to stackView
// width and height 2-pts greater (so we can "outline" the stackView frame)
dashedView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: 2),
dashedView.heightAnchor.constraint(equalTo: stackView.heightAnchor, constant: 2),
dashedView.centerXAnchor.constraint(equalTo: stackView.centerXAnchor),
dashedView.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
])
// add touchUp target
testButton.addTarget(self, action: #selector(self.didTap(_:)), for: .touchUpInside)
// this will track the text for updating titleView's label
idx = sampleData.count
updateText()
}
func updateText() -> Void {
// change the text in titleView's titleLabel
// let auto-layout handle ALL of the resizing
titleView.titleLabel.text = sampleData[idx % sampleData.count]
idx += 1
}
@objc func didTap(_ sender: Any) {
updateText()
}
}
class TitleView: UIView {
// TitleView has a multi-line UILabel
// with 20-pts "padding" on each side
// and 12-pts "padding" on top and bottom
var titleLabel: UILabel = {
let v = UILabel()
v.text = "Title Label"
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .green
[titleLabel].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
[=10=].backgroundColor = UIColor(white: 0.9, alpha: 1.0)
[=10=].textAlignment = .center
addSubview([=10=])
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
])
// we want this label's text to control its height
titleLabel.setContentHuggingPriority(.required, for: .vertical)
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class ContentView: UIView {
// ContentView has two labels and a "round" view
var labelA: UILabel = {
let v = UILabel()
v.text = "Label A"
return v
}()
var labelB: UILabel = {
let v = UILabel()
v.text = "The Content View is Yellow"
return v
}()
var roundView: RoundView = {
let v = RoundView()
v.backgroundColor = .orange
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .yellow
[labelA, labelB].forEach {
[=10=].translatesAutoresizingMaskIntoConstraints = false
[=10=].backgroundColor = .cyan
[=10=].textAlignment = .center
addSubview([=10=])
}
roundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(roundView)
NSLayoutConstraint.activate([
// constrain labelA 20-pts from top / leading
labelA.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
labelA.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// constrain roundView 20-pts from trailing
roundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// constrain labelA trailing 20-pts from roundView leading
labelA.trailingAnchor.constraint(equalTo: roundView.leadingAnchor, constant: -20.0),
// constrain roundView height equal to lableA height
roundView.heightAnchor.constraint(equalTo: labelA.heightAnchor),
// keep roundView square (1:1 ratio)
roundView.widthAnchor.constraint(equalTo: roundView.heightAnchor),
// center roundView vertically to labelA
roundView.centerYAnchor.constraint(equalTo: labelA.centerYAnchor),
// labelB top is 8-pts below labelA
labelB.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
// constrain labelB 20-pts from leading / trailing / bottom
labelB.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
labelB.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// keep labelA and labelB heights equal
labelA.heightAnchor.constraint(equalTo: labelB.heightAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
}
}
class RoundView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
// update cornerRadius here to keep it round
layer.cornerRadius = bounds.size.width * 0.5
}
}
class DashedBorderView: UIView {
// simple view with dashed border
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor(red: 1.0, green: 0.25, blue: 0.25, alpha: 1.0).cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
print(NSStringFromClass(type(of: self)), #function, bounds)
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
注意:当视图收到对 layoutSubviews()
的调用时,它们也会打印 () 它们的 class 名称和边界,因此您会在调试控制台中看到类似这样的内容:
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 146.0)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 49.0, 49.0)
SampleApp.TitleView layoutSubviews() (0.0, 0.0, 295.0, 105.5)
SampleApp.ContentView layoutSubviews() (0.0, 0.0, 295.0, 186.5)
SampleApp.RoundView layoutSubviews() (0.0, 0.0, 69.0, 69.5)