如何以从下到上增长的方式将 stackView 固定到视图
How to pin stackView to a view in such a way that it grows from bottom to top
我在 scrollView 中有一个 stackView,我想固定 stackView,使其从底部开始,随着向其添加更多视图而向上增长。
显示当前行为的图像显示 stackView 固定到视图的顶部,内容从顶部到底部。
显示所需行为的图像,显示堆栈视图从下到上增长。
当前代码:
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
}()
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
}
}
ViewController:
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"))
}
}
我希望视图在到达顶部时以这种方式运行:
要让“CellView 堆栈”从底部增长,您需要将该堆栈视图嵌入到另一个“容器”视图中。
- 将 stackView 添加为 stackContainer 视图的子视图
- 约束 stackView Leading / Trailing / Bottom 都等于零
- 将 stackView Top 大于或等于 限制为零...这将使 stackView 保持在底部,但会强制 stackContainer 在获取时增长足够高
- 将 stackContainer 视图作为子视图添加到 scrollView
- 将该 stackContainer 视图的所有 4 个边都约束到 scrollView 的
.contentLayoutGuide
以控制“可滚动区域”。
- 将 stackContainer 的 width 限制为 scrollView 的
.frameLayoutGuide
width
“棘手”的部分是这样的:我们还限制了 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
}
}
我在 scrollView 中有一个 stackView,我想固定 stackView,使其从底部开始,随着向其添加更多视图而向上增长。
显示当前行为的图像显示 stackView 固定到视图的顶部,内容从顶部到底部。
显示所需行为的图像,显示堆栈视图从下到上增长。
当前代码:
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
}()
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
}
}
ViewController:
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"))
}
}
我希望视图在到达顶部时以这种方式运行:
要让“CellView 堆栈”从底部增长,您需要将该堆栈视图嵌入到另一个“容器”视图中。
- 将 stackView 添加为 stackContainer 视图的子视图
- 约束 stackView Leading / Trailing / Bottom 都等于零
- 将 stackView Top 大于或等于 限制为零...这将使 stackView 保持在底部,但会强制 stackContainer 在获取时增长足够高
- 将 stackContainer 视图作为子视图添加到 scrollView
- 将该 stackContainer 视图的所有 4 个边都约束到 scrollView 的
.contentLayoutGuide
以控制“可滚动区域”。 - 将 stackContainer 的 width 限制为 scrollView 的
.frameLayoutGuide
width
“棘手”的部分是这样的:我们还限制了 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
}
}