问题描述
我在 scrollView 中有一个 stackView,我想固定 stackView,使其从底部开始并随着添加更多视图而向上增长。
显示当前行为的图像,显示固定在视图顶部的 stackView 以及从顶部到底部的内容。
显示所需行为的图像,显示堆栈视图从下到上增长。
当前代码:
自定义堆栈视图
open class CustomStackView: UIView {
public var scrollView: UIScrollView = {
let view = UIScrollView(frame: .zero)
view.isScrollEnabled = true
view.bounces = true
view.alwaysBounceVertical = true
view.keyboarddismissMode = .interactive
view.layoutMargins = .zero
view.clipsToBounds = false
return view
}()
public var stackView: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fillProportionally
let gap: CGFloat = 0
stackView.spacing = gap
stackView.layoutMargins = UIEdgeInsets(top: gap,left: gap,bottom: gap,right: gap)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}()
override public init(frame: CGRect) {
super.init(frame: frame)
self.layoutMargins = .zero
backgroundColor = .green
scrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(scrollView)
NSLayoutConstraint.activate([scrollView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),scrollView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor),scrollView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor),scrollView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor)])
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),stackView.topAnchor.constraint(lessthanorEqualTo: scrollView.topAnchor),stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)])
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CellView
class CellView: UIView {
private let title: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = .black
label.font = .systemFont(ofSize: 18,weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .left
label.text = "Title"
return label
}()
private let subTitle: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = .lightGray
label.font = .systemFont(ofSize: 14,weight: .regular)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "subTitle"
label.isHidden = true
return label
}()
private let seperatorView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .lightGray
view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let headerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .leading
stackView.distribution = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(frame: CGRect) {
super.init(frame: .zero)
setupView()
}
init(title: String) {
super.init(frame: .zero)
setupView()
configureView(with: title)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
self.backgroundColor = .gray
headerStackView.addArrangedSubview(title)
headerStackView.addArrangedSubview(subTitle)
addSubview(seperatorView)
addSubview(headerStackView)
setupConstraints()
}
private func setupConstraints() {
self.heightAnchor.constraint(equalToConstant: 50).isActive = true
headerStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 16).isActive = true
headerStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
headerStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
seperatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
seperatorView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
seperatorView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
}
private func configureView(with title: String) {
self.title.text = title
}
}
视图控制器:
class TestViewController: UIViewController {
private let containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.red
view.clipsToBounds = true
return view
}()
private let customStackView: CustomStackView = {
let view = CustomStackView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
configureView()
}
func setupView() {
view.addSubview(containerView)
containerView.clipsToBounds = true
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: view.topAnchor,constant: 40),containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),])
containerView.addSubview(customStackView)
NSLayoutConstraint.activate([
customStackView.topAnchor.constraint(equalTo: containerView.topAnchor),customStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),customStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),customStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),])
}
func configureView() {
customStackView.stackView.addArrangedSubview(CellView(title: "Title"))
customStackView.stackView.addArrangedSubview(CellView(title: "Tesla"))
customStackView.stackView.addArrangedSubview(CellView(title: "Lucid"))
customStackView.stackView.addArrangedSubview(CellView(title: "Merc"))
customStackView.stackView.addArrangedSubview(CellView(title: "BMW"))
}
}
我希望视图在到达顶部时以这种方式运行:
解决方法
为了让您的“CellViews 堆栈”从底部开始增长,您需要将该 stackView 嵌入另一个“容器”视图中。
- 将stackView添加为stackContainer视图的子视图
- 将 stackView 前导/尾随/底部限制为零
- 将 stackView 顶部 大于或等于 限制为零...这将使 stackView 保持在底部,但会强制 stackContainer 在它足够高时增长
- 将stackContainer视图作为子视图添加到scrollView
- 将该 stackContainer 视图的所有 4 个边限制到 scrollView 的
.contentLayoutGuide
以控制“可滚动区域”。 - 将 stackContainer 的 width 限制为 scrollView 的
.frameLayoutGuide
宽度
“棘手”的部分是这样的:我们还限制 stackContainer 视图的 height 等于 scrollView 的 .frameLayoutGuide
高度,但我们将该约束的优先级设置为小于要求-- 例如 .defaultHigh
。这将使 stackContainer 视图保持等于滚动视图框架的高度,直到堆栈视图变得足够高以使其增长。
这是运行时的样子:
Red: your main "container" view
Green: CustomStackView
Yellow: scrollView
Teal: stackContainerView
Orange: stack view
Gray: CellViews
这是经过一些修改的代码...
CustomStackView -- 查看代码中的注释:
open class CustomStackView: UIView {
public var scrollView: UIScrollView = {
let view = UIScrollView(frame: .zero)
view.isScrollEnabled = true
view.bounces = true
view.alwaysBounceVertical = true
view.keyboardDismissMode = .interactive
view.layoutMargins = .zero
view.clipsToBounds = false
return view
}()
// a UIView to hold the stack view
let stackContainerView: UIView = {
let v = UIView()
v.backgroundColor = .systemTeal
return v
}()
// func to "auto-scroll" to the bottom of the scroll view
public func scrollToBottom() -> Void {
let sz = scrollView.contentSize
let offset = sz.height - scrollView.frame.height
UIView.animate(withDuration: 0.3,animations: {
self.scrollView.contentOffset.y = offset
})
}
public var stackView: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill //Proportionally
let gap: CGFloat = 0
stackView.spacing = gap
stackView.layoutMargins = UIEdgeInsets(top: gap,left: gap,bottom: gap,right: gap)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}()
override public init(frame: CGRect) {
super.init(frame: frame)
self.layoutMargins = .zero
backgroundColor = .green
scrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(scrollView)
NSLayoutConstraint.activate([scrollView.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor),scrollView.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor),scrollView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor),scrollView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor)])
stackContainerView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
// add stackContainer to the scrollView
scrollView.addSubview(stackContainerView)
// add stackView to the stackContainer
stackContainerView.addSubview(stackView)
let contentGuide = scrollView.contentLayoutGuide
let frameGuide = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stackContainer sides to scrollView's .contentLayoutGuide
stackContainerView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),stackContainerView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor),stackContainerView.topAnchor.constraint(equalTo: contentGuide.topAnchor),stackContainerView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor),// constrain stackContainer width to scrollView's .frameLayoutGuide
stackContainerView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor),// constrain stackView sides to stackContainer
stackView.leadingAnchor.constraint(equalTo: stackContainerView.leadingAnchor),stackView.trailingAnchor.constraint(equalTo: stackContainerView.trailingAnchor),// constrain stackView to bottom of container
stackView.bottomAnchor.constraint(equalTo: stackContainerView.bottomAnchor),// keep stackView Top >= stackContainer Top
stackView.topAnchor.constraint(greaterThanOrEqualTo: stackContainerView.topAnchor),])
// constrain stackContainer Height to scrollView's .frameLayoutGuide Height
let cHeight = stackContainerView.heightAnchor.constraint(equalTo: frameGuide.heightAnchor)
// give it less-than .required Priority,so it can grow
// when the stackView gets taller
cHeight.priority = .defaultHigh
// activate this constraint
cHeight.isActive = true
// so we can see view frames for debugging during dev
scrollView.backgroundColor = .yellow
stackView.backgroundColor = .orange
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
TestViewController -- 唯一的变化是添加了点击手势识别器...每次点击都会在堆栈底部添加一个新的 CellView
:
class TestViewController: UIViewController {
private let containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.red
view.clipsToBounds = true
return view
}()
private let customStackView: CustomStackView = {
let view = CustomStackView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
configureView()
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self,action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
@objc func didTap(_ g: UITapGestureRecognizer) -> Void {
// on tap,get the current number of "cell views" in the custom stack view
let n = customStackView.stackView.arrangedSubviews.count
// add a new CellView
customStackView.stackView.addArrangedSubview(CellView(title: "Cell \(n)"))
// scroll the stack view to the bottom to show the newly added CellView
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1,execute: {
self.customStackView.scrollToBottom()
})
}
func setupView() {
view.addSubview(containerView)
containerView.clipsToBounds = true
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: view.topAnchor,constant: 40),containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),])
containerView.addSubview(customStackView)
NSLayoutConstraint.activate([
customStackView.topAnchor.constraint(equalTo: containerView.topAnchor),customStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),customStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),customStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),])
}
func configureView() {
customStackView.stackView.addArrangedSubview(CellView(title: "Title"))
customStackView.stackView.addArrangedSubview(CellView(title: "Tesla"))
customStackView.stackView.addArrangedSubview(CellView(title: "Lucid"))
customStackView.stackView.addArrangedSubview(CellView(title: "Merc"))
customStackView.stackView.addArrangedSubview(CellView(title: "BMW"))
}
}
CellView -- 没有变化,只是为了完成将它包含在这里:
class CellView: UIView {
private let title: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = .black
label.font = .systemFont(ofSize: 18,weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .left
label.text = "Title"
return label
}()
private let subTitle: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = .lightGray
label.font = .systemFont(ofSize: 14,weight: .regular)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "subTitle"
label.isHidden = true
return label
}()
private let seperatorView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .lightGray
view.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let headerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .leading
stackView.distribution = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(frame: CGRect) {
super.init(frame: .zero)
setupView()
}
init(title: String) {
super.init(frame: .zero)
setupView()
configureView(with: title)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
self.backgroundColor = .gray
headerStackView.addArrangedSubview(title)
headerStackView.addArrangedSubview(subTitle)
addSubview(seperatorView)
addSubview(headerStackView)
setupConstraints()
}
private func setupConstraints() {
self.heightAnchor.constraint(equalToConstant: 50).isActive = true
headerStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor,constant: 16).isActive = true
headerStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
headerStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
seperatorView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
seperatorView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
seperatorView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
}
private func configureView(with title: String) {
self.title.text = title
}
}