UIScrollView 内容插入由子树外的视图高度定义
UIScrollView content inset defined by a height of a view outside of the subtree
给定一个视图层次结构:
- View `container`
- UIScrollView `scrollView`
- UIView `content`
- UIView `footer`
我希望 UIScrollView.contentInset.bottom
等于 footer.bounds.height
。
问题:这个可以用Auto Layout表达吗?
现在,有一个非常明显的 brute-force 方法,我知道并且有效:
- 观察
footer
的 bounds
属性 的变化
scrollView.contentInset.bottom = -footer.bounds.height
一旦 footer
的 parent 完成 layoutSubviews()
.
或者我可以在 content.bottom
和 scrollView.bottom
之间有一个约束(我相信你知道,它表示 non-ambiguously 大小的底部内容插图content) 并在每次 footer
边界改变时改变它的常量。
但关键是所有这些方法都非常 on-the-nose,它们产生的糟糕代码真的让我感到不舒服所以我想知道:
这可以用Auto Layout表达吗?
我已尝试执行以下操作:
content.bottomAnchor.constraint(equalTo: footer.topAnchor)
希望 content.bottomAnchor
将被视为滚动视图内容的底部插图,但没有 - 自动布局实际上将其视为我将内容的底部限制在页脚的顶部。
好的 - 一种方法...
从 iOS 11 开始(我假设您不需要早于此定位),UIScrollView
的子视图可以限制为滚动视图的 Frame Layout Guide
.这使得向滚动视图层次结构添加非滚动 UI 元素变得容易。
基于此层次结构:
- view
- scrollView
- contentView
- element1
- element2
- element3
- UILayoutGuide
- footerView
我们要做的是:
- 将所有 "scrollable" 元素添加到 contentView
- plus 添加一个
UILayoutGuide
到 contentView 将用作或 "bottom" 可滚动元素
- 将 footerView 添加到 scrollView last 因此它位于 z 顺序的顶部
- 将 footerView 约束到 scrollView 的
Frame Layout Guide
,使其保持原样
- 约束我们
UILayoutGuide
的heightAnchor等于footerView 的heightAnchor
因为 UILayoutGuide
是非渲染视图,所以它不可见,但会从我们最后一个 viewable[=69= 的底部创建 space ] 元素到 contentView 的底部——它会自动改变高度 if/when footerView 改变高度。
这是一个完整的示例 - scrollView / contentView / 3 imageViews / layout guide / translucent footerView:
class ExampleViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .lightGray
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .cyan
return v
}()
let footerView: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.textColor = .white
v.font = UIFont.systemFont(ofSize: 24.0, weight: .bold)
v.text = "Footer View"
v.backgroundColor = UIColor.black.withAlphaComponent(0.65)
return v
}()
var imgView1: UIImageView = {
let v = UIImageView()
v.backgroundColor = .red
v.image = UIImage(systemName: "1.circle")
v.tintColor = .white
return v
}()
var imgView2: UIImageView = {
let v = UIImageView()
v.backgroundColor = .green
v.image = UIImage(systemName: "2.circle")
v.tintColor = .white
return v
}()
var imgView3: UIImageView = {
let v = UIImageView()
v.backgroundColor = .blue
v.image = UIImage(systemName: "3.circle")
v.tintColor = .white
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add 3 image views as the content we want to see
contentView.addSubview(imgView1)
contentView.addSubview(imgView2)
contentView.addSubview(imgView3)
// add contentView to srollView
scrollView.addSubview(contentView)
// add footer view to scrollView last so it's at the top of the z-order
scrollView.addSubview(footerView)
view.addSubview(scrollView)
[scrollView, contentView, footerView, imgView1, imgView2, imgView3].forEach {
[=11=].translatesAutoresizingMaskIntoConstraints = false
}
// "spacer" for bottom of scroll content
// we'll constrain it to the height of the footer view
let spacerGuide = UILayoutGuide()
contentView.addLayoutGuide(spacerGuide)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let scFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView view 40-pts on all 4 sides to view (safe-area)
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
// contentView view 0-pts top / leading / trailing / bottom to scrollView contentLayoutGuide
contentView.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
// contentView width == scrollView frameLayoutGuide width
contentView.widthAnchor.constraint(equalTo: scFLG.widthAnchor, constant: 0.0),
// imgView1 to top of contentView
imgView1.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
// imgView1 width / height
imgView1.widthAnchor.constraint(equalToConstant: 240.0),
imgView1.heightAnchor.constraint(equalToConstant: 240.0),
// imgView1 centerX to contentView centerX
imgView1.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView2 top to bottom of imgView1 + 20-pt spacing
imgView2.topAnchor.constraint(equalTo: imgView1.bottomAnchor, constant: 20.0),
// imgView2 width / height
imgView2.widthAnchor.constraint(equalToConstant: 200.0),
imgView2.heightAnchor.constraint(equalToConstant: 280.0),
// imgView2 centerX to contentView centerX
imgView2.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView3 top to bottom of imgView2 + 20-pt spacing
imgView3.topAnchor.constraint(equalTo: imgView2.bottomAnchor, constant: 20.0),
// imgView3 width / height
imgView3.widthAnchor.constraint(equalToConstant: 280.0),
imgView3.heightAnchor.constraint(equalToConstant: 320.0),
// imgView3 centerX to contentView centerX
imgView3.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// spacerGuide top to bottom of actual content
// spacerGuide top to imgView3 bottom
spacerGuide.topAnchor.constraint(equalTo: imgView3.bottomAnchor, constant: 0.0),
// spacerGuide to leading / trailing / bottom of contentView
spacerGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
spacerGuide.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
spacerGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
// footerView to leading / trailing / bottom of scrollView frameLayoutGuide
// (constrained to frameLayoutGuide so it won't scroll)
footerView.leadingAnchor.constraint(equalTo: scFLG.leadingAnchor, constant: 0.0),
footerView.trailingAnchor.constraint(equalTo: scFLG.trailingAnchor, constant: 0.0),
footerView.bottomAnchor.constraint(equalTo: scFLG.bottomAnchor, constant: 0.0),
// footerView height == scrollView height with 0.25 multiplier
// (so it will change height when scrollView changes height, such as device rotation)
footerView.heightAnchor.constraint(equalTo: scFLG.heightAnchor, multiplier: 0.25),
// finally, spacerGuide height equal to footerView height
spacerGuide.heightAnchor.constraint(equalTo: footerView.heightAnchor),
])
}
}
结果:
滚动到底部:
并旋转(因此我们看到 footerView 高度发生变化)一直滚动到底部:
编辑
具体问题的答案是:你不能。
滚动视图的 contentInset
不是可以添加约束的对象...它是滚动视图的 属性。就像您无法将滚动视图的 .backgroundColor
约束到自动布局约束一样。
给定一个视图层次结构:
- View `container`
- UIScrollView `scrollView`
- UIView `content`
- UIView `footer`
我希望 UIScrollView.contentInset.bottom
等于 footer.bounds.height
。
问题:这个可以用Auto Layout表达吗?
现在,有一个非常明显的 brute-force 方法,我知道并且有效:
- 观察
footer
的 scrollView.contentInset.bottom = -footer.bounds.height
一旦footer
的 parent 完成layoutSubviews()
.
bounds
属性 的变化
或者我可以在 content.bottom
和 scrollView.bottom
之间有一个约束(我相信你知道,它表示 non-ambiguously 大小的底部内容插图content) 并在每次 footer
边界改变时改变它的常量。
但关键是所有这些方法都非常 on-the-nose,它们产生的糟糕代码真的让我感到不舒服所以我想知道:
这可以用Auto Layout表达吗?
我已尝试执行以下操作:
content.bottomAnchor.constraint(equalTo: footer.topAnchor)
希望 content.bottomAnchor
将被视为滚动视图内容的底部插图,但没有 - 自动布局实际上将其视为我将内容的底部限制在页脚的顶部。
好的 - 一种方法...
从 iOS 11 开始(我假设您不需要早于此定位),UIScrollView
的子视图可以限制为滚动视图的 Frame Layout Guide
.这使得向滚动视图层次结构添加非滚动 UI 元素变得容易。
基于此层次结构:
- view
- scrollView
- contentView
- element1
- element2
- element3
- UILayoutGuide
- footerView
我们要做的是:
- 将所有 "scrollable" 元素添加到 contentView
- plus 添加一个
UILayoutGuide
到 contentView 将用作或 "bottom" 可滚动元素 - 将 footerView 添加到 scrollView last 因此它位于 z 顺序的顶部
- 将 footerView 约束到 scrollView 的
Frame Layout Guide
,使其保持原样 - 约束我们
UILayoutGuide
的heightAnchor等于footerView 的heightAnchor
因为 UILayoutGuide
是非渲染视图,所以它不可见,但会从我们最后一个 viewable[=69= 的底部创建 space ] 元素到 contentView 的底部——它会自动改变高度 if/when footerView 改变高度。
这是一个完整的示例 - scrollView / contentView / 3 imageViews / layout guide / translucent footerView:
class ExampleViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .lightGray
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .cyan
return v
}()
let footerView: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.textColor = .white
v.font = UIFont.systemFont(ofSize: 24.0, weight: .bold)
v.text = "Footer View"
v.backgroundColor = UIColor.black.withAlphaComponent(0.65)
return v
}()
var imgView1: UIImageView = {
let v = UIImageView()
v.backgroundColor = .red
v.image = UIImage(systemName: "1.circle")
v.tintColor = .white
return v
}()
var imgView2: UIImageView = {
let v = UIImageView()
v.backgroundColor = .green
v.image = UIImage(systemName: "2.circle")
v.tintColor = .white
return v
}()
var imgView3: UIImageView = {
let v = UIImageView()
v.backgroundColor = .blue
v.image = UIImage(systemName: "3.circle")
v.tintColor = .white
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add 3 image views as the content we want to see
contentView.addSubview(imgView1)
contentView.addSubview(imgView2)
contentView.addSubview(imgView3)
// add contentView to srollView
scrollView.addSubview(contentView)
// add footer view to scrollView last so it's at the top of the z-order
scrollView.addSubview(footerView)
view.addSubview(scrollView)
[scrollView, contentView, footerView, imgView1, imgView2, imgView3].forEach {
[=11=].translatesAutoresizingMaskIntoConstraints = false
}
// "spacer" for bottom of scroll content
// we'll constrain it to the height of the footer view
let spacerGuide = UILayoutGuide()
contentView.addLayoutGuide(spacerGuide)
let g = view.safeAreaLayoutGuide
let svCLG = scrollView.contentLayoutGuide
let scFLG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView view 40-pts on all 4 sides to view (safe-area)
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
// contentView view 0-pts top / leading / trailing / bottom to scrollView contentLayoutGuide
contentView.topAnchor.constraint(equalTo: svCLG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor, constant: 0.0),
// contentView width == scrollView frameLayoutGuide width
contentView.widthAnchor.constraint(equalTo: scFLG.widthAnchor, constant: 0.0),
// imgView1 to top of contentView
imgView1.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0),
// imgView1 width / height
imgView1.widthAnchor.constraint(equalToConstant: 240.0),
imgView1.heightAnchor.constraint(equalToConstant: 240.0),
// imgView1 centerX to contentView centerX
imgView1.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView2 top to bottom of imgView1 + 20-pt spacing
imgView2.topAnchor.constraint(equalTo: imgView1.bottomAnchor, constant: 20.0),
// imgView2 width / height
imgView2.widthAnchor.constraint(equalToConstant: 200.0),
imgView2.heightAnchor.constraint(equalToConstant: 280.0),
// imgView2 centerX to contentView centerX
imgView2.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// imgView3 top to bottom of imgView2 + 20-pt spacing
imgView3.topAnchor.constraint(equalTo: imgView2.bottomAnchor, constant: 20.0),
// imgView3 width / height
imgView3.widthAnchor.constraint(equalToConstant: 280.0),
imgView3.heightAnchor.constraint(equalToConstant: 320.0),
// imgView3 centerX to contentView centerX
imgView3.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// spacerGuide top to bottom of actual content
// spacerGuide top to imgView3 bottom
spacerGuide.topAnchor.constraint(equalTo: imgView3.bottomAnchor, constant: 0.0),
// spacerGuide to leading / trailing / bottom of contentView
spacerGuide.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0),
spacerGuide.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0),
spacerGuide.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0),
// footerView to leading / trailing / bottom of scrollView frameLayoutGuide
// (constrained to frameLayoutGuide so it won't scroll)
footerView.leadingAnchor.constraint(equalTo: scFLG.leadingAnchor, constant: 0.0),
footerView.trailingAnchor.constraint(equalTo: scFLG.trailingAnchor, constant: 0.0),
footerView.bottomAnchor.constraint(equalTo: scFLG.bottomAnchor, constant: 0.0),
// footerView height == scrollView height with 0.25 multiplier
// (so it will change height when scrollView changes height, such as device rotation)
footerView.heightAnchor.constraint(equalTo: scFLG.heightAnchor, multiplier: 0.25),
// finally, spacerGuide height equal to footerView height
spacerGuide.heightAnchor.constraint(equalTo: footerView.heightAnchor),
])
}
}
结果:
滚动到底部:
并旋转(因此我们看到 footerView 高度发生变化)一直滚动到底部:
编辑
具体问题的答案是:你不能。
滚动视图的 contentInset
不是可以添加约束的对象...它是滚动视图的 属性。就像您无法将滚动视图的 .backgroundColor
约束到自动布局约束一样。