Reputation: 119
I want my footer UIStackView
to hug its content when views are laid out, and take priority over the UIScrollView
. Currently the header UIStackView
and main body UIScrollView
hug its contents, causing the footer UIStackView
to expand, therefore leaving a lot of space below its contents and not looking like it's not pinned to the bottom. I would like the header(UIStackView) and footer(UIStackView) to hug its contents, and the main body(UIScrollView) to expand as needed.
I have a UIViewController
with the following view hierarchy
UIViewController
-UIView
-UIStackView(header)
-ScrollView(scrollable main body)
-UIView
-UIStackView
-UIStackView(footer)
Requirements for header and footer:
self.view.addSubview(self.headerStackView)
NSLayoutConstraint.activate([
self.headerStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.headerStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.headerStackView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
])
self.view.addSubview(self.scrollView)
NSLayoutConstraint.activate([
self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.headerStackView.bottomAnchor)
])
self.view.addSubview(self.footerStackView)
NSLayoutConstraint.activate([
self.footerStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.footerStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.footerStackView.topAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
self.footerStackView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
])
Upvotes: 1
Views: 731
Reputation: 119
It turns out thew view within the footer stackview was not constrained properly. Adding the missing constraint fixed the issue.
Upvotes: -1
Reputation: 77423
You can do this by constraining the Top of the footer view to the Bottom of the scroll view's content, but with .priority = .defaultLow
, then constrain the Bottom of the footer view to less-than-or-equal-to the Bottom of the scroll view's frame.
Here's how it can look...
view
scrollView
A quick example:
class FooterVC: UIViewController {
let scrollView = UIScrollView()
let headerStack = UIStackView()
let footerStack = UIStackView()
let scrollContentLabel = UILabel()
var numLines: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// MARK: setup the header stack view with add/remove buttons
let addButton = UIButton()
addButton.setTitle("Add", for: [])
addButton.setTitleColor(.white, for: .normal)
addButton.setTitleColor(.lightGray, for: .highlighted)
addButton.backgroundColor = .systemRed
let removeButton = UIButton()
removeButton.setTitle("Remove", for: [])
removeButton.setTitleColor(.white, for: .normal)
removeButton.setTitleColor(.lightGray, for: .highlighted)
removeButton.backgroundColor = .systemRed
headerStack.axis = .horizontal
headerStack.alignment = .center
headerStack.spacing = 20
headerStack.backgroundColor = .systemYellow
// a couple re-usable objects
var vSpacer: UIView!
vSpacer = UIView()
vSpacer.widthAnchor.constraint(equalToConstant: 16.0).isActive = true
headerStack.addArrangedSubview(vSpacer)
headerStack.addArrangedSubview(addButton)
headerStack.addArrangedSubview(removeButton)
vSpacer = UIView()
vSpacer.widthAnchor.constraint(equalToConstant: 16.0).isActive = true
headerStack.addArrangedSubview(vSpacer)
// MARK: setup the footer stack view
footerStack.axis = .vertical
footerStack.spacing = 8
footerStack.backgroundColor = .systemGreen
["Footer Stack View", "with Two Labels"].forEach { str in
let vLabel = UILabel()
vLabel.text = str
vLabel.textAlignment = .center
vLabel.font = .systemFont(ofSize: 24.0, weight: .regular)
vLabel.textColor = .yellow
footerStack.addArrangedSubview(vLabel)
}
// MARK: setup scroll content
scrollContentLabel.font = .systemFont(ofSize: 44.0, weight: .light)
scrollContentLabel.numberOfLines = 0
scrollContentLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// so we can see the scroll view
scrollView.backgroundColor = .systemBlue
[scrollContentLabel, footerStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(v)
}
[headerStack, scrollView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// header stack at top of view
headerStack.topAnchor.constraint(equalTo: g.topAnchor),
headerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
headerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
headerStack.heightAnchor.constraint(equalToConstant: 72.0),
// make buttons equal widths
addButton.widthAnchor.constraint(equalTo: removeButton.widthAnchor),
// scroll view Top to header stack Bottom
scrollView.topAnchor.constraint(equalTo: headerStack.bottomAnchor),
// other 3 sides
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
// scroll content...
// let's inset the content label 12-points on each side
// to make it easier to see the framing
scrollContentLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 12.0),
scrollContentLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -12.0),
scrollContentLabel.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -24.0),
// top and bottom to content layout guide
scrollContentLabel.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
scrollContentLabel.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// footer stack view - leading/trailing to frame layout guide
footerStack.leadingAnchor.constraint(equalTo: fg.leadingAnchor),
footerStack.trailingAnchor.constraint(equalTo: fg.trailingAnchor),
])
var tmpConstraint: NSLayoutConstraint!
// now, we want the footer to stick to the bottom of the content,
// but allow auto-layout to break the constraint when needed
tmpConstraint = footerStack.topAnchor.constraint(equalTo: scrollContentLabel.bottomAnchor)
tmpConstraint.priority = .defaultLow
tmpConstraint.isActive = true
// and we want the footer to stop at the bottom of the scroll view frame
// default is .required, but we'll set it here for emphasis
tmpConstraint = footerStack.bottomAnchor.constraint(lessThanOrEqualTo: fg.bottomAnchor)
tmpConstraint.priority = .required
tmpConstraint.isActive = true
// actions for the buttons
addButton.addTarget(self, action: #selector(addContent(_:)), for: .touchUpInside)
removeButton.addTarget(self, action: #selector(removeContent(_:)), for: .touchUpInside)
// add the first line to the content
addContent(nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// we need to set the bottom inset of the scroll view content
// so it can scroll up above the footer stack view
let h: CGFloat = footerStack.frame.height
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: h, right: 0)
}
@objc func addContent(_ sender: Any?) {
// add another line of text to the content
numLines += 1
scrollContentLabel.text = (1...numLines).map({"Line \($0)"}).joined(separator: "\n")
// scroll newly added line into view
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
let r = CGRect(x: 0, y: self.scrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
self.scrollView.scrollRectToVisible(r, animated: true)
})
}
@objc func removeContent(_ sender: Any?) {
numLines -= 1
numLines = max(1, numLines)
scrollContentLabel.text = (1...numLines).map({"Line \($0)"}).joined(separator: "\n")
}
}
and it will look like this when running:
As we add content (in this case, just adding more lines to a label), it will "push down" the footer view until it hits the bottom of the scroll view's frame... at which point we can scroll behind the footer view.
Upvotes: 3