have a vertical stackview take all the spacing inside another stackview - ios

I am trying to achieve the following layout in a table cell (it has a dynamic height):
The result I am getting is that 2nd stackview doesn't stretch to add more height to fill the multiline label.
lazy var stackView: StackView = {
let subviews: [UIView] = []
let view = StackView(arrangedSubviews: subviews)
view.axis = .horizontal
view.alignment = .center
self.containerView.addSubview(view)
view.snp.makeConstraints({ (make) in
make.edges.equalToSuperview().inset(inset)
})
return view
}()
lazy var detailsStackView: StackView = {
let subviews: [UIView] = []
let view = StackView(arrangedSubviews: subviews)
view.axis = .vertical
stackView.alignment = .fill
return view
}()
lazy var nameLabel: Label = {
let view = Label(style: .styleBoldSize24)
return view
}()
let detailsLabel: Label = {
let label = Label(style: .styleRegular25)
label.textAlignment = .left
label.numberOfLines = 10
label.text = "Where does it come from Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of anything embarrassing hidden in the mid words etc."
return label
}()
lazy var avatarImageView: ImageView = {
let view = ImageView(frame: CGRect())
view.snp.makeConstraints({ (make) in
make.width.height.equalTo(100)
})
return view
}()
override func setup() {
super.setup()
stackView.axis = .horizontal
stackView.alignment = .top
stackView.addArrangedSubview(avatarImageView)
detailsStackView.spacing = self.inset
detailsStackView.addArrangedSubview(nameLabel)
detailsStackView.addArrangedSubview(detailsLabel)
stackView.addArrangedSubview(avatarImageView)
stackView.addArrangedSubview(detailsStackView)
stackView.snp.remakeConstraints({ (make) in
let inset = self.inset
make.edges.equalToSuperview().inset(UIEdgeInsets(top: inset/2, left: inset, bottom: inset/2, right: inset))
})}

Related

UILabel skeleton not showing while it's inside a stack view

Simply, I'm trying to display the skeleton for two UILabels that are subviews of a stack view. When I'm saying label.isSkeletonable = true it doesn't work at all. However, when I make the stack view isSkeletonable = true it works and becomes like the picture below
class ArticleCellView: UITableViewCell {
// MARK: - *** Properties ***
static let cellIdentifier = "ArticleTableViewCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.isSkeletonable = true
contentView.isSkeletonable = true
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - *** UI Elements ***
lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 13.0, weight: .medium)
label.isSkeletonable = true
return label
}()
lazy var abstractLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 12.0)
label.isSkeletonable = true
return label
}()
lazy var thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.frame.size.width = 100
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
imageView.isSkeletonable = true
return imageView
}()
lazy var stackView: UIStackView = {
let stack = UIStackView()
stack.contentMode = .scaleToFill
stack.distribution = .fillEqually
stack.spacing = 20
stack.isSkeletonable = true
return stack
}()
func configure() {
// Adding subviews
contentView.addSubview(thumbnailImageView)
contentView.addSubview(stackView)
stackView.addSubview(titleLabel)
stackView.addSubview(abstractLabel)
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.width.height.equalTo(100)
$0.centerY.equalToSuperview()
}
stackView.snp.makeConstraints {
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
$0.trailing.equalToSuperview().inset(10)
$0.top.bottom.equalTo(thumbnailImageView)
}
titleLabel.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.trailing.equalToSuperview().inset(2)
$0.top.equalToSuperview().inset(10)
}
abstractLabel.snp.makeConstraints {
$0.leading.equalToSuperview().inset(10)
$0.trailing.equalToSuperview().inset(2)
$0.top.equalTo(titleLabel.snp.bottom).offset(10)
$0.bottom.equalToSuperview().offset(2)
}
}
}
As far as you can tell, no other solution out there worked for me even clipsToBounds did nothing. I'm using the SkeletonView from the following: https://github.com/Juanpe/SkeletonView
First, you need to add the labels as arranged subviews of the stack view.
So, this:
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(abstractLabel)
instead of this:
stackView.addSubview(titleLabel)
stackView.addSubview(abstractLabel)
Next, because you want the labels to be arranged Vertically, set the stack view axis (and spacing) accordingly:
stack.axis = .vertical
stack.spacing = 10
and, because we want the stack view to arrange the labels, don't set any constraints on the labels:
// titleLabel.snp.makeConstraints {
// $0.leading.equalToSuperview().inset(10)
// $0.trailing.equalToSuperview().inset(2)
// $0.top.equalToSuperview().inset(10)
// }
//
// abstractLabel.snp.makeConstraints {
// $0.leading.equalToSuperview().inset(10)
// $0.trailing.equalToSuperview().inset(2)
// $0.top.equalTo(titleLabel.snp.bottom).offset(10)
// $0.bottom.equalToSuperview().offset(2)
// }
and finally, it is a really good idea to add comments so you understand what your code is trying to do:
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
// image view leading is 10-points from superview (contentView) leading
$0.leading.equalToSuperview().inset(10)
// 100 x 100
$0.width.height.equalTo(100)
// top and bottom constrained to top and bottom of superview (contenView)
$0.top.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
// stackView leading is 10=points from image view trailing
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
// 10-points from superview (contentView) trailing
$0.trailing.equalToSuperview().inset(10)
// stackView centered vertically to image view
$0.centerY.equalTo(thumbnailImageView)
}
Here's the full, modified version of your cell class:
class ArticleCellView: UITableViewCell {
// MARK: - *** Properties ***
static let cellIdentifier = "ArticleTableViewCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// from the SkeletonView docs:
// SkeletonView is recursive, so if you want show the skeleton in all skeletonable views,
// you only need to call the show method in the main container view. For example, with UIViewControllers.
// So, we only need to set it on self
self.isSkeletonable = true
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - *** UI Elements ***
lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 13.0, weight: .medium)
return label
}()
lazy var abstractLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.textAlignment = .natural
label.textColor = UIColor(named: "AccentColor")
label.font = UIFont.systemFont(ofSize: 12.0)
return label
}()
lazy var thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.frame.size.width = 100
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
return imageView
}()
lazy var stackView: UIStackView = {
let stack = UIStackView()
stack.distribution = .fillEqually
// we want Vertical Stack View
stack.axis = .vertical
stack.spacing = 10
return stack
}()
// MARK: - *** Methods ***
override func prepareForReuse() {
super.prepareForReuse()
thumbnailImageView.kf.cancelDownloadTask()
thumbnailImageView.image = nil
}
func configure() {
// Adding subviews
contentView.addSubview(thumbnailImageView)
contentView.addSubview(stackView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(abstractLabel)
// Setting up the constraints
thumbnailImageView.snp.makeConstraints {
// leading is 10-points from superview (contentView) leading
$0.leading.equalToSuperview().inset(10)
// 100 x 100
$0.width.height.equalTo(100)
// top and bottom constrained to top and bottom of superview (contenView)
$0.top.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
// stackView leading is 10=points from image view trailing
$0.leading.equalTo(thumbnailImageView.snp.trailing).offset(10)
// 10-points from superview (contentView) trailing
$0.trailing.equalToSuperview().inset(10)
// stackView centered vertically to image view
$0.centerY.equalTo(thumbnailImageView)
}
}
}

How to put a stack view below another stack view

I want to place the dateInfo stack view below the userInfoStack view.
This is what it currently looks like
How can I place the stack view vertically one below the other? I tried adjusting the distribution, but it doesn't solve the problem.
I am trying to create reusable banner in the UI.
class UserCell: UIView {
var rootStack = UIStackView()
var userInfoStackView = UIStackView()
var dateInfoStackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
setUPViews()
addComponents()
layoutComponents()
}
private func setUPViews(){
rootStack = UIStackView()
rootStack.translatesAutoresizingMaskIntoConstraints = false
rootStack.alignment = .center
rootStack.distribution = .fillEqually
rootStack.spacing = 5
userInfoStackView = UIStackView()
userInfoStackView.axis = .horizontal
userInfoStackView.spacing = 10
dateInfoStackView = UIStackView()
dateInfoStackView.axis = .vertical
dateInfoStackView.spacing = 2
}
private func addComponents() {
userInfoStackView.addArrangedSubview(nameLabel)
userInfoStackView.addArrangedSubview(compCode)
dateInfoStackView.addArrangedSubview(captionLabel)
dateInfoStackView.addArrangedSubview(dateLabel2)
}
private func layoutComponents() {
addSubview(rootStack)
rootStack.addArrangedSubview(userInfoStackView)
rootStack.addArrangedSubview(dateInfoStackView)
rootStack.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
}
}
The axis on the rootStack is not set
Try rootStack.axis = .vertical
since the default value for axis of a UIStackView is horizontal your views are laid out horizontally next to each other
Also set to rootStack.alignment = .leading to align everything to the left margin
So your implementation should look like below
rootStack = UIStackView()
rootStack.translatesAutoresizingMaskIntoConstraints = false
rootStack.axis = .vertical
rootStack.alignment = .leading
rootStack.distribution = .fillEqually
rootStack.spacing = 5

How to center two views in super view with greater than or equal to constraints

I made an example ViewController with two Labels to highlight my issue. The goal is to vertically separate the labels by 10, and then center them vertically using greater than or equal to constraints. I'm using visual format, but this should apply if I setup my constraints like view.topAnchor.constraint(greaterThan.... I also have two constraints to horizontally layout the labels
My ViewController:
class myVC: UIViewController {
lazy var titleLabel: UILabel = {
let l = UILabel(frame: .zero)
l.translatesAutoresizingMaskIntoConstraints = false
l.text = "Hello World"
l.font = .systemFont(ofSize: 50)
l.textColor = .black
return l
}()
lazy var descLabel: UILabel = {
let l = UILabel(frame: .zero)
l.translatesAutoresizingMaskIntoConstraints = false
l.text = "description"
l.font = .systemFont(ofSize: 35)
l.textColor = .gray
return l
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
view.addSubview(titleLabel)
view.addSubview(descLabel)
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
descLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-(<=50)-[titleLabel]-(10)-[descLabel]-(<=50)-|", options: .init(), metrics: nil, views: ["titleLabel": titleLabel, "descLabel": descLabel]))
}
}
This results in . From my understanding, this SHOULD separate the views by 10 pts, and center the labels vertically because in the format "V:|-(<=50)-[titleLabel]-(10)-[descLabel]-(<=50)-|" I say that the distance between the Title Label's top and the superView's top should be at least (greaterThanOrEqualTo) 50, and the distance between the description Label's bottom and the superView's bottom should be at least 50. What should my top and bottom constraints look like if I want to center the two labels vertically?
Yes, I realize I can just set vertical and horizontal centers, but this is an example I made for a problem I can't use those for. I need to be able to center the View with greater(or less) than or equal to constraints.
It's very difficult to center elements using VFL.
It's also difficult to center two elements unless they are embedded in a UIView or a UIStackView.
Here is one option by embedding the labels in a "container" UIView:
class MyVC: UIViewController {
lazy var titleLabel: UILabel = {
let l = UILabel(frame: .zero)
l.translatesAutoresizingMaskIntoConstraints = false
l.text = "Hello World"
l.font = .systemFont(ofSize: 50)
l.textColor = .black
// center the text in the label - change to .left if desired
l.textAlignment = .center
return l
}()
lazy var descLabel: UILabel = {
let l = UILabel(frame: .zero)
l.translatesAutoresizingMaskIntoConstraints = false
l.text = "description"
l.font = .systemFont(ofSize: 35)
l.textColor = .gray
// center the text in the label - change to .left if desired
l.textAlignment = .center
return l
}()
lazy var containerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// give the labels and containerView background colors to make it easy to see the layout
titleLabel.backgroundColor = .green
descLabel.backgroundColor = .cyan
containerView.backgroundColor = .blue
// add containerView to view
view.addSubview(containerView)
// add labels to containerView
containerView.addSubview(titleLabel)
containerView.addSubview(descLabel)
NSLayoutConstraint.activate([
// constrain titleLabel Top to containerView Top
titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor),
// constrain titleLabel Leading and Trailing to containerView Leading and Trailing
titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
// constrain descLabel Leading and Trailing to containerView Leading and Trailing
descLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
descLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
// constrain descLabel Bottom to containerView Bottom
descLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
// constrain descLabel Top 10-pts from titleLabel Bottom
descLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10.0),
// constrain containerView centered horizontally and vertically
containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
Result:
This can be achieved easily by using stackview. Add both the labels in stackview and center it vertically in the superview with all other constraints(top, leading, bottom, trailing).
Here is the sample code of view controller for your use-case.
class ViewController: UIViewController {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Hello \nWorld"
label.font = .systemFont(ofSize: 50)
label.backgroundColor = .orange
label.numberOfLines = 0
label.textColor = .black
return label
}()
lazy var descLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "a\n b\n c\n"
label.font = .systemFont(ofSize: 35)
label.backgroundColor = .green
label.numberOfLines = 0
label.textColor = .gray
return label
}()
lazy var contentView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 10
stackView.distribution = .fill
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
contentView.addArrangedSubview(titleLabel)
contentView.addArrangedSubview(descLabel)
self.view.addSubview(contentView)
let constraints = [
contentView.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor),
contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
contentView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
The above code will result this view and it goes on to take the top and buttom space until it meets the safeArea. Moreover you can set the vertical content hugging and compression resistance priority to control which label to expand or shrink.

Embedd StackView in ScrollView that is embedded in a main StackView

Embedd StackView in ScrollView that is embedded in a main StackView
I am having trouble with a rather complicated detail view that I want to do programmatically. My view hierarchy looks something like this:
Since this might be better explained visualising, I have a screenshot here:
My problem is that I don't know how to set the height constraint on descriptionTextView – right now it's set to 400. What I want though is that it takes up all the space available as the middle item of the main stack view. Once one or more comments are added to the contentStackView, the text field should shrink.
I am not sure which constraints for which views I must set to achieve this...
Here's my take on it so far:
import UIKit
class DetailSampleViewController: UIViewController {
lazy var mainStackView: UIStackView = {
let m = UIStackView()
m.axis = .vertical
m.alignment = .fill
m.distribution = .fill
m.spacing = 10
m.translatesAutoresizingMaskIntoConstraints = false
m.addArrangedSubview(titleTextField)
m.addArrangedSubview(contentScrollView)
m.addArrangedSubview(footerStackView)
return m
}()
lazy var titleTextField: UITextField = {
let t = UITextField()
t.borderStyle = .roundedRect
t.placeholder = "Some Fancy Placeholder"
t.text = "Some Fancy Title"
t.translatesAutoresizingMaskIntoConstraints = false
return t
}()
lazy var contentScrollView: UIScrollView = {
let s = UIScrollView()
s.contentMode = .scaleToFill
s.keyboardDismissMode = .onDrag
s.translatesAutoresizingMaskIntoConstraints = false
s.addSubview(contentStackView)
return s
}()
lazy var contentStackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
s.distribution = .equalSpacing
s.spacing = 10
s.contentMode = .scaleToFill
s.addArrangedSubview(descriptionTextView)
s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "And..."))
s.addArrangedSubview(getCommentLabel(with: "..even..."))
s.addArrangedSubview(getCommentLabel(with: "...more..."))
s.addArrangedSubview(getCommentLabel(with: "...comments..."))
s.addArrangedSubview(getCommentLabel(with: "Some fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "Another fancy comment"))
s.addArrangedSubview(getCommentLabel(with: "And..."))
s.addArrangedSubview(getCommentLabel(with: "..even..."))
s.addArrangedSubview(getCommentLabel(with: "...more..."))
s.addArrangedSubview(getCommentLabel(with: "...comments..."))
return s
}()
lazy var descriptionTextView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 17.0)
tv.clipsToBounds = true
tv.layer.cornerRadius = 5.0
tv.layer.borderWidth = 0.25
tv.translatesAutoresizingMaskIntoConstraints = false
tv.text = """
Some fancy textfield text,
spanning over multiple
lines
...
"""
return tv
}()
lazy var footerStackView: UIStackView = {
let f = UIStackView()
f.axis = .horizontal
f.alignment = .fill
f.distribution = .fillEqually
let commentLabel = UILabel()
commentLabel.text = "Comments"
let addCommentButton = UIButton(type: UIButton.ButtonType.system)
addCommentButton.setTitle("Add Comment", for: .normal)
f.addArrangedSubview(commentLabel)
f.addArrangedSubview(addCommentButton)
return f
}()
override func loadView() {
view = UIView()
view.backgroundColor = . systemBackground
navigationController?.isToolbarHidden = true
view.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),
contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
descriptionTextView.heightAnchor.constraint(equalToConstant: 400),
descriptionTextView.leadingAnchor.constraint(equalTo: mainStackView.leadingAnchor),
descriptionTextView.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
])
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Detail View"
}
func getCommentLabel(with text: String) -> UILabel {
let l = UILabel()
l.layer.borderWidth = 0.25
l.translatesAutoresizingMaskIntoConstraints = false
l.text = text
return l
}
}
You're close, but a couple notes:
When using stack views - particularly inside scroll views - you sometimes need to explicitly define which elements can be stretched or not, and which elements can be compressed or not.
To get the scroll view filled before it has enough content, you need to set constraints so the combined content height is equal to the scroll view frame's height, but give that constraint a low priority so auto-layout can "break" it when you have enough vertical content.
A personal preference: I'm generally not a fan of adding subviews inside lazy var declarations. It can become confusing when trying to setup constraints.
I've re-worked your posted code to at least get close to what you're going for. It starts with NO comment labels... tapping the "Add Comment" button will add "numbered comment labels" and every third comment will wrap onto multiple lines.
Not really all that much in the way of changes... and I think I added enough comments to make things clear.
class DetailSampleViewController: UIViewController {
lazy var mainStackView: UIStackView = {
let m = UIStackView()
m.axis = .vertical
m.alignment = .fill
m.distribution = .fill
m.spacing = 10
m.translatesAutoresizingMaskIntoConstraints = false
// don't add subviews here
return m
}()
lazy var titleTextField: UITextField = {
let t = UITextField()
t.borderStyle = .roundedRect
t.placeholder = "Some Fancy Placeholder"
t.text = "Some Fancy Title"
t.translatesAutoresizingMaskIntoConstraints = false
return t
}()
lazy var contentScrollView: UIScrollView = {
let s = UIScrollView()
s.contentMode = .scaleToFill
s.keyboardDismissMode = .onDrag
s.translatesAutoresizingMaskIntoConstraints = false
// don't add subviews here
return s
}()
lazy var contentStackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
// distribution needs to be .fill (not .equalSpacing)
s.distribution = .fill
s.spacing = 10
s.contentMode = .scaleToFill
// don't add subviews here
return s
}()
lazy var descriptionTextView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 17.0)
tv.clipsToBounds = true
tv.layer.cornerRadius = 5.0
tv.layer.borderWidth = 0.25
tv.translatesAutoresizingMaskIntoConstraints = false
tv.text = """
Some fancy textfield text,
spanning over multiple lines.
This textView now has a minimum height of 160-pts.
"""
return tv
}()
lazy var footerStackView: UIStackView = {
let f = UIStackView()
f.axis = .horizontal
f.alignment = .fill
f.distribution = .fillEqually
let commentLabel = UILabel()
commentLabel.text = "Comments"
let addCommentButton = UIButton(type: UIButton.ButtonType.system)
addCommentButton.setTitle("Add Comment", for: .normal)
// add a target so we can add comment labels
addCommentButton.addTarget(self, action: #selector(addCommentLabel(_:)), for: .touchUpInside)
// don't allow button height to be compressed
addCommentButton.setContentCompressionResistancePriority(.required, for: .vertical)
f.addArrangedSubview(commentLabel)
f.addArrangedSubview(addCommentButton)
return f
}()
// just for demo - numbers the added comment labels
var commentIndex: Int = 0
// do all this in viewDidLoad(), not in loadView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = . systemBackground
navigationController?.isToolbarHidden = true
title = "Detail View"
// add the mainStackView
view.addSubview(mainStackView)
// add elements to mainStackView
mainStackView.addArrangedSubview(titleTextField)
mainStackView.addArrangedSubview(contentScrollView)
mainStackView.addArrangedSubview(footerStackView)
// add contentStackView to contentScrollView
contentScrollView.addSubview(contentStackView)
// add descriptionTextView to contentStackView
contentStackView.addArrangedSubview(descriptionTextView)
// tell contentStackView to be the height of contentScrollView frame
let contentStackHeight = contentStackView.heightAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.heightAnchor)
// but give it a lower priority do it can grow as comment labels are added
contentStackHeight.priority = .defaultLow
NSLayoutConstraint.activate([
// constrain mainStackView top / bottom / leading / trailing to safe area
mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
// title text field
titleTextField.heightAnchor.constraint(equalToConstant: titleTextField.intrinsicContentSize.height),
// minimum height for descriptionTextView
descriptionTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 160.0),
// constrain contentStackView top / leading / trailing / bottom to contentScrollView
contentStackView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
contentStackView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
// constrain contentStackView width to contentScrollView frame
contentStackView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor),
// activate contentStackHeight constraint
contentStackHeight,
])
// during dev, give some background colors so we can see the frames
contentScrollView.backgroundColor = .cyan
descriptionTextView.backgroundColor = .yellow
}
#objc func addCommentLabel(_ sender: Any?) -> Void {
// commentIndex is just used to number the added comments
commentIndex += 1
// let's make every third label end up with multiple lines, just to
// confirm variable-height labels won't mess things up
var s = "This is label \(commentIndex)"
if commentIndex % 3 == 0 {
s += ", and it has enough text that it should need to wrap onto multiple lines, even in landscape orientation."
}
let v = getCommentLabel(with: s)
// don't let comment labels stretch vertically
v.setContentHuggingPriority(.required, for: .vertical)
// don't let comment labels get compressed vertically
v.setContentCompressionResistancePriority(.required, for: .vertical)
contentStackView.addArrangedSubview(v)
// auto-scroll to bottom to show newly added comment label
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
let r = CGRect(x: 0.0, y: self.contentScrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
self.contentScrollView.scrollRectToVisible(r, animated: true)
}
}
func getCommentLabel(with text: String) -> UILabel {
let l = UILabel()
l.layer.borderWidth = 0.25
l.translatesAutoresizingMaskIntoConstraints = false
l.text = text
// allow wrapping / multi-line comments
l.numberOfLines = 0
return l
}
}

Choose which subview on a stackView will stretch (Programmatically)

I have a display of horizontal stack views, which subviews consists on a label and a textField. the stackView is constrained with the borders of the view
I'm trying to stretch my textField subview to so it fills the remaining space of the stack, while the label's stacks adjusts to fit the label size itself. But the inverse is happening. I've tried many solutions but nothing helped me. All the views and constraints we're made programmatically.
For my stack, I'm using:
func customTextField() -> UIStackView {
let stack: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.isLayoutMarginsRelativeArrangement = true
sv.alignment = .leading
sv.backgroundColor = .red
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
let label: UILabel = {
let lb = UILabel()
lb.backgroundColor = .red
lb.text = "Label is here"
lb.translatesAutoresizingMaskIntoConstraints = false
return lb
}()
let textField: UITextField = {
let tf = UITextField()
tf.backgroundColor = .blue
tf.text = "Text Field"
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
stack.addArrangedSubview(label)
stack.addArrangedSubview(textField)
return stack
}
the caller of my customTextField:
let profileUserStack: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
stack.contentMode = .scaleToFill
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
self.addSubview(profileUserStack)
for i in profileTextViews {
let view = self.customTextField()
profileUserStack.addArrangedSubview(view)
}
constraints.append(profileUserStack.buildConstraint(toItem: perfilImageView, constant: 32, type: .top, baseItem: .bottom))
constraints.append(profileUserStack.buildConstraint(toItem: self, constant: 16, type: .leading, baseItem: .leading))
constraints.append(profileUserStack.buildConstraint(toItem: self, constant: -16, type: .trailing, baseItem: .trailing))
activateConstraints(&constraints, to: self)
The results:
https://imgur.com/a/ccFZlRM
Notice that's exactly what I want to achieve. But I want the textField to be stretched.
Set contentHuggingPriority to your label such that it always stays as the size of its content and textField takes remaining space.
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)

Resources