Embedd StackView in ScrollView that is embedded in a main StackView - ios

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
}
}

Related

Swift: UIView not resizing to fit content

I am building an app in which I have a list of Reviews. They look like the following screenshot
For some reason, I am failing to make the UIView (Gray box from the top light gray line to the bottom one) resize correctly. The white text inside it (Actual review) is longer than 1 line and should not get cut off, only when reaching a maximum of say 5 lines. The thing is, it works when I don't set width and height constraints for the user image you see on the left side. Removing those will make the view resize correctly, but it will completely distort the image. The image top and botton anchors seem to be glued to the anchors in its horizontal stackview, which again are stuck to the UIView's top and botton anchors with constants, as it should. But nowhere do I say that the UIView should always have the size of the image. I don't get why it wont go bigger than the image.
Here is a screenshot of my structure with the constraints, hope it is clear enough:
NSLayoutConstraint.activate([
//Main horizontal stackview (one Rating is the name of the UIView)
hStack.leadingAnchor.constraint(equalTo: oneRating.leadingAnchor, constant: 23),
hStack.trailingAnchor.constraint(equalTo: oneRating.trailingAnchor, constant: -18),
hStack.topAnchor.constraint(equalTo: oneRating.topAnchor, constant: 15),
hStack.bottomAnchor.constraint(equalTo: oneRating.bottomAnchor, constant: -13),
reviewerImage.heightAnchor.constraint(equalToConstant: 80),
reviewerImage.widthAnchor.constraint(equalToConstant: 80),
//Limit the size of the Review Text to make sure its always at the same spot
v2Stack.widthAnchor.constraint(equalToConstant: 220.0),
])
//Verified checkmark constraints
if isUserVerified == true {
reviewerVerified.bottomAnchor.constraint(equalTo: reviewerImage.bottomAnchor).isActive = true
reviewerVerified.trailingAnchor.constraint(equalTo: reviewerImage.trailingAnchor, constant: -2).isActive = true
}
I know it is hard to help like this but I have tried to fix this for a few days and no matter what I do, I can't get it to work.
EDIT:
As per request, here is the code I have to add the ImageView to my UIView().
//Add Image
let reviewerImage = UIImageView()
reviewerImage.contentMode = .scaleAspectFill
reviewerImage.layer.cornerRadius = 40 //= 1/2 of width, because we hard coded the size
reviewerImage.image = UIImage(named: "person-icon") //Placeholder. Download image here
reviewerImage.translatesAutoresizingMaskIntoConstraints = false
VStack1.addArrangedSubview(reviewerImage)
You will need to add a few more sizing constraints, but...
The "trick" is to embed your "reviewer image view" in a clear "container" view. Then constrain the image view to the Top of that container.
Here is some sample code that gets close to your layout:
class JanView: UIView {
let reviewerImageView: UIImageView = {
let v = UIImageView()
return v
}()
let starImageView: UIImageView = {
let v = UIImageView()
return v
}()
let chevronImageView: UIImageView = {
let v = UIImageView()
return v
}()
let nameLabel: UILabel = {
let v = UILabel()
return v
}()
let locLabel: UILabel = {
let v = UILabel()
return v
}()
let reviewTextLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 5
return v
}()
let publishedLabel: UILabel = {
let v = UILabel()
return v
}()
let starValueLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .darkGray
let outerHStack: UIStackView = {
let v = UIStackView()
v.spacing = 10
return v
}()
let labelsVStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let starsAndChevronVStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let starsHStack: UIStackView = {
let v = UIStackView()
v.spacing = 0
v.alignment = .center
return v
}()
// review image container
let reviewerImageContainer: UIView = {
let v = UIView()
return v
}()
[nameLabel, reviewTextLabel].forEach { v in
v.textColor = .white
}
[locLabel, publishedLabel].forEach { v in
v.textColor = .lightGray
}
starValueLabel.textColor = .systemYellow
outerHStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(outerHStack)
[nameLabel, locLabel, reviewTextLabel, publishedLabel].forEach { v in
labelsVStack.addArrangedSubview(v)
}
[starValueLabel, starImageView].forEach { v in
starsHStack.addArrangedSubview(v)
}
[starsHStack, chevronImageView].forEach { v in
starsAndChevronVStack.addArrangedSubview(v)
}
[reviewerImageContainer, labelsVStack, starsAndChevronVStack].forEach { v in
outerHStack.addArrangedSubview(v)
}
// add reviewer image view to container
reviewerImageContainer.addSubview(reviewerImageView)
reviewerImageView.translatesAutoresizingMaskIntoConstraints = false
// specific properties
reviewerImageView.contentMode = .scaleAspectFill
reviewerImageView.layer.cornerRadius = 40
reviewerImageView.layer.masksToBounds = true
let cfg = UIImage.SymbolConfiguration(pointSize: 12.0, weight: .bold)
if let img = UIImage(systemName: "star.fill", withConfiguration: cfg) {
starImageView.image = img
}
starImageView.tintColor = .systemYellow
starImageView.contentMode = .center
starValueLabel.text = "4"
if let img = UIImage(systemName: "chevron.right", withConfiguration: cfg) {
chevronImageView.image = img
}
chevronImageView.tintColor = .lightGray
chevronImageView.contentMode = .center
nameLabel.text = "Name Here"
locLabel.text = "Location Here"
reviewTextLabel.text = "Review Text Here"
publishedLabel.text = "Published Info Here"
let g = self
// to get the 2nd vertical stack view to fit (horizontally) to its content
let sacWidth = starsAndChevronVStack.widthAnchor.constraint(equalToConstant: 10.0)
sacWidth.priority = .defaultHigh
let vPadding: CGFloat = 12
let hPadding: CGFloat = 10
NSLayoutConstraint.activate([
outerHStack.topAnchor.constraint(equalTo: g.topAnchor, constant: vPadding),
outerHStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: hPadding),
outerHStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -hPadding),
outerHStack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -vPadding),
reviewerImageView.widthAnchor.constraint(equalToConstant: 80.0),
reviewerImageView.heightAnchor.constraint(equalTo: reviewerImageView.widthAnchor),
// align the reviewer image view with the top of the container view
reviewerImageView.topAnchor.constraint(equalTo: reviewerImageContainer.topAnchor),
reviewerImageView.leadingAnchor.constraint(equalTo: reviewerImageContainer.leadingAnchor),
reviewerImageView.trailingAnchor.constraint(equalTo: reviewerImageContainer.trailingAnchor),
// give the stars value label a width, so it doesn't vary by text
// "5" is wider than "1" (or it may be "" ?)
starValueLabel.widthAnchor.constraint(equalToConstant: 16.0),
// make the star image view square
starImageView.widthAnchor.constraint(equalTo: starImageView.heightAnchor),
// make the stars HStack height equal to the stars label height
starsHStack.heightAnchor.constraint(equalTo: starValueLabel.heightAnchor),
sacWidth,
])
}
}
and an example controller:
class ReviewVC: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
let reviewsStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
[reviewsStack, scrollView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
scrollView.addSubview(reviewsStack)
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
reviewsStack.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
reviewsStack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
reviewsStack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
reviewsStack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
reviewsStack.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
let sampleLocs: [String] = [
"Koblenz, Germany",
"Westerwald, Germany",
"Bonn, Germany",
"Saarbrüken, Germany",
]
let sampleRevs: [String] = [
"For some reason, I am failing to make the UIView (Gray box from the top light gray line to the bottom one) resize correctly.",
"A Single Line",
"The white text inside it (Actual review) is longer than 1 line and should not get cut off, only when reaching a maximum of say 5 lines.",
"The thing is, it works when I don't set width and height constraints for the user image you see on the left side. Removing those will make the view resize correctly, but it will completely distort the image.",
"Another Single Line",
"The image top and botton anchors seem to be glued to the anchors in its horizontal stackview, which again are stuck to the UIView's top and botton anchors with constants, as it should.",
]
let sampleStars: [String] = [
"5", "4", "3", "2", "1",
]
for i in 0..<sampleRevs.count {
let v = JanView()
v.nameLabel.text = "Clara R."
v.locLabel.text = sampleLocs[i % sampleLocs.count]
v.reviewTextLabel.text = sampleRevs[i]
v.publishedLabel.text = "Published less than 24h ago"
v.starValueLabel.text = sampleStars[i % sampleStars.count]
if let img = UIImage(named: "prof") {
v.reviewerImageView.image = img
}
reviewsStack.addArrangedSubview(v)
let sepView = UIView()
sepView.backgroundColor = .lightGray
sepView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true
reviewsStack.addArrangedSubview(sepView)
}
}
}
Here is how it ends up looking:

UIView width and height adjustment according to it subview programatically

I want to create something like this:
There's a white box under the buttons. If we are using SwiftUI logic, it's vertical padding : 5 and horizontal padding : 10, to create it with SwiftUI is pretty easy, but from what I have learned there is no padding and background color to a UIStackView and to create something like this, you need a UIView then add the stack view on top of the UIView.
This is what I have done so far:
//
// TransaksiViewController.swift
// HaselWiratama
//
// Created by Farhandika on 18/09/21.
// Copyright © 2021 Hasel.id. All rights reserved.
//
import UIKit
class TransaksiViewController: UIViewController {
let pesanButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let ambulanceButton: BigButton = {
let button = BigButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .blue
button.configure(viewModel: MyCustomBigButton(title: "Phone",
imageName: "house", isSystemImage: false))
return button
}()
let topStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.spacing = 10
return stackView
}()
let uiView = UIView()
func configureUIView() {
//Configure the stackview
topStackView.addArrangedSubview(pesanButton)
topStackView.addArrangedSubview(ambulanceButton)
// add stack to UIView
uiView.addSubview(topStackView)
NSLayoutConstraint.activate([
topStackView.heightAnchor.constraint(equalToConstant: 150),
topStackView.centerYAnchor.constraint(equalTo: uiView.centerYAnchor),
topStackView.centerXAnchor.constraint(equalTo: uiView.centerXAnchor)
])
uiView.translatesAutoresizingMaskIntoConstraints = false
uiView.backgroundColor = .purple
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .cyan
view.addSubview(uiView)
configureUIView()
NSLayoutConstraint.activate([
uiView.widthAnchor.constraint(equalToConstant: 500),
uiView.heightAnchor.constraint(equalToConstant: 400),
uiView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
uiView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
/* Ignore the button width and height because I have not add the constraint yet */
The result:
As you can see, the width and height of the UIView is not relative to its child view.
How do I emulate the same padding horizontal 10 and vertical 5 in UIView?
(Basically a progressive or responsive width and height of a UIView.)
Since you have mentioned that you used constraints, see the following code. It reflect a UIViewController with what you need:
class ViewController: UIViewController {
// Horizontal Stackview
lazy var stack: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.spacing = 10 // Inter-item space
view.backgroundColor = .white
view.distribution = .fillEqually // Setting distribution to fill equally
return view
}()
// Button 1
lazy var button1: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 1", for: .normal)
button.backgroundColor = .red
return button
}()
// Button 2
lazy var button2: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Button 2", for: .normal)
button.backgroundColor = .blue
return button
}()
// View that holds the stackview
lazy var stackHolder: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
view.addSubview(stackHolder)
stackHolder.addSubview(stack)
stack.addArrangedSubview(button1)
stack.addArrangedSubview(button2)
//Setting layout constraints
NSLayoutConstraint.activate([
stackHolder.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackHolder.centerYAnchor.constraint(equalTo: view.centerYAnchor),
// Setting a width and height of the stack so that the `stackHolder` adjust relatively
stack.widthAnchor.constraint(equalToConstant: 250),
stack.heightAnchor.constraint(equalToConstant: 80),
// Setting the constraints with `constant` values for padding
stack.leadingAnchor.constraint(equalTo: stackHolder.leadingAnchor, constant: 5),
stack.trailingAnchor.constraint(equalTo: stackHolder.trailingAnchor, constant: -5),
stack.topAnchor.constraint(equalTo: stackHolder.topAnchor, constant: 10),
stack.bottomAnchor.constraint(equalTo: stackHolder.bottomAnchor, constant: -10),
])
}
}
This will output:

UIStackView container view height based on subviews

Here is my simple example. I have 1 vertical stack view with 1 subview. I want that subviews height to be based on the intrinsic height of the label within it, so that I can maintain a dynamic height for the entire stack view. How can this be done? Thanks
I think you did it right. But here is the keys:
Don't set height for stackView.
Set label top, bottom, left, trailing constraint to view.
Run. It should be okay on simulator.
If you found label's height seems not wrapping (neither both on storyboard or simulator), then change label's Vertical Content Hugging Priority to 750.
Try this code:
class DyanmicTextLabelViewController: UIViewController {
private var didAddConstraint = false
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.text = "Layout anchors let you create constraints in an easy-to-read, compact format. They expose a number of methods for creating different types of constraints, as shown in Listing 13-1."
view.numberOfLines = 0
return view
}()
private lazy var container: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.backgroundColor = .red
return view
}()
private lazy var stackview : UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(container)
return view
}()
override func loadView() {
super.loadView()
view.addSubview(stackview)
view.setNeedsUpdateConstraints()
view.backgroundColor = .white
}
override func updateViewConstraints() {
super.updateViewConstraints()
if didAddConstraint == false {
didAddConstraint = true
// stackview constraints
stackview.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
let topAnchor = stackview.topAnchor.constraint(equalTo: view.topAnchor)
topAnchor.constant = 20
topAnchor.isActive = true
stackview.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// label constraint
// example for giving label a left padding
let labelLeft = label.leftAnchor.constraint(equalTo: container.leftAnchor)
labelLeft.constant = 16.0
labelLeft.isActive = true
label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
label.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
}
}
}
The important part here is the initialization of stackview, label & constraint set on label
label initialization
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.text = "Layout anchors let you create constraints in an easy-to-read, compact format. They expose a number of methods for creating different types of constraints, as shown in Listing 13-1."
view.numberOfLines = 0
return view
}()
stackview initialization
private lazy var stackview : UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(container)
return view
}()
label constraint
// label constraint
// example for giving label a left padding
let labelLeft = label.leftAnchor.constraint(equalTo: container.leftAnchor)
labelLeft.constant = 16.0
labelLeft.isActive = true
label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
label.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
This settings could be easily translated to storyboard.

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)

Set button width to fit dynamic button title

I have my UI structured say Level 1(UP), Level 2(DOWN) with some controls
In level 1, I have a label L1
In level 2, I have a button and label L2
In level 2 my button may be removed in runtime and I wanted my label L2 to be aligned to leading edge as L1
I'm facing two problems here
When I set my button title programmatically, I want to set my button such that its width grows when text increases and reduces its width when there is less text content. This isn't happening. Please see below screens the constraints I've in place
When I removed my button from superview, I wanted my L2 label Leading to be aligned to L1 leading. So I created a constraint from L2.leading = L1.leading and prioirty is 999
In this case, the button gets reduces its size to almost 0 even if i have text in that. Please advice me setting this up
Problem #1:
use .horizontal UIStackview for the button and text. set its distribution to .fill. For the button set contentCompression resistance priority to .required for .horizontal & set contenHugging priority to .required for .horizontal. So the Button will always wrap the text no matter what.
Problem #2:
While placing inside a stackview, you don't have to remove the button from superview. Just hide it using isHidden.
Code Demonstration
class SampleVC: UIViewController {
private var didAddConstraint = false
// Basic Views
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.text = "Label"
return view
}()
private let topButton: UIButton = {
let view = UIButton()
view.translatesAutoresizingMaskIntoConstraints = false
view.setTitle("Button", for: .normal)
view.setTitleColor(.gray, for: .highlighted)
view.backgroundColor = .green
view.setContentHuggingPriority(.required, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
private let rightLabel: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.numberOfLines = 0
view.text = "label"
view.backgroundColor = .red
return view
}()
private lazy var stackview: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(topButton)
view.addArrangedSubview(rightLabel)
return view
}()
override func loadView() {
super.loadView()
view.addSubview(label)
view.addSubview(stackview)
view.setNeedsUpdateConstraints()
view.backgroundColor = .white
}
override func updateViewConstraints() {
super.updateViewConstraints()
if didAddConstraint == false {
didAddConstraint = true
// top label
label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
label.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// stackview
stackview.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
stackview.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8.0).isActive = true
stackview.rightAnchor.constraint(equalToSystemSpacingAfter: view.rightAnchor, multiplier: 16.0).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// TEST Code
// topButton.setTitle("TEST TEST TEST", for: .normal)
// topButton.isHidden = true
}
}

Resources