Swift: UIView not resizing to fit content - ios

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:

Related

Change the size of my component dynamically based on the amount of text in my label

I have a simple component, a label and an imageview.
They are put in horizontal position. I need to center that and if my label gets more text, the component will change too and the position of my image will change. I have to do this programmatically and I’m really new to programmatically constraints.
For more explanation this is an example:
And this is my code right now:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
progressText.sizeToFit()
progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_success")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView.image = resizedImage
iconView.sizeToFit()
iconView.layoutIfNeeded()
return iconView
}()
let iconView2: UIImageView = {
let iconView2 = UIImageView()
iconView2.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_error")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView2.image = resizedImage
iconView2.sizeToFit()
iconView2.layoutIfNeeded()
return iconView2
}()
private func setupView() {
let overlayView = UIView()
overlayView.translatesAutoresizingMaskIntoConstraints = false
addSubview(overlayView)
overlayView.addSubview(iconView)
overlayView.addSubview(iconView2)
NSLayoutConstraint.activate([
iconView.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
iconView.centerYAnchor.constraint(equalTo: iconView.centerYAnchor),
progressIndicator.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
progressIndicator.centerYAnchor.constraint(equalTo: iconView2.centerYAnchor),
iconView2.topAnchor.constraint(equalTo: self.topAnchor, constant: 20),
iconView2.widthAnchor.constraint(equalToConstant: 10),
iconView2.heightAnchor.constraint(equalToConstant: 10),
iconView2.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -10)
])
addSubview(progressText)
NSLayoutConstraint.activate([
progressText.leadingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: 15),
progressText.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Also, this is how I set this in a parent view:
private func setupProgressBar() {
self.codeValidationTextField.addSubview(progressBar)
progressBar.translatesAutoresizingMaskIntoConstraints = false
progressBar.leadingAnchor.constraint(equalTo: codeValidationTextField.leadingAnchor, constant: 10).isActive = true
progressBar.bottomAnchor.constraint(equalTo: codeValidationTextField.bottomAnchor, constant: -33).isActive = true
progressBar.widthAnchor.constraint(equalTo: codeValidationTextField.widthAnchor, multiplier: 0.5)
progressBar.progressIndicator.isHidden = false
progressBar.iconView.isHidden = true
progressBar.progressText.isHidden = false
}
It's not entirely clear what you are trying to do...
your code references a progressIndicator when setting constraints, but there is no progressIndicator identified.
there is no reason to be "scaling" your images... the image view will do that for you.
you're adding 2 "icon" image views, but the layout doesn't make much sense.
there is no need to use a overlayView -- we can add the subviews to the custom view itself (and, the code you've shown doesn't setup that overlayView correctly, so no idea if it is supposed to be doing anything).
Note that it helps to include comments in your code, telling yourself what you expect to happen.
And, a tip: during development, give your UI elements contrasting background colors to make it easy to see the framing.
So, let's simplify this and start with a single image view and label:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
// no need for any of this
//progressText.sizeToFit()
//progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
if let image = UIImage(named: "feedback_success") {
iconView.image = image
} else {
// could not load image, so set background color
iconView.backgroundColor = .systemRed
}
// no need for any of this
//let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
//let resizedImage = renderer.image { _ in
// image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
//}
//iconView.image = resizedImage
//iconView.sizeToFit()
//iconView.layoutIfNeeded()
return iconView
}()
private func setupView() {
// no need for this... add subviews to self
//let overlayView = UIView()
//overlayView.translatesAutoresizingMaskIntoConstraints = false
//addSubview(overlayView)
self.addSubview(iconView)
self.addSubview(progressText)
NSLayoutConstraint.activate([
// 20-points on top and bottom of iconView
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.0),
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.0),
// align iconView to leading-edge of self
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
// iconView 16 x 16
iconView.widthAnchor.constraint(equalToConstant: 16),
iconView.heightAnchor.constraint(equalToConstant: 16),
// progressText centered vertically
progressText.centerYAnchor.constraint(equalTo: self.centerYAnchor),
// progressText 15-points from iconView trailing edge
progressText.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 15.0),
// align progressText to trailing-edge of self
progressText.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
])
// during development, use some background colors so we can see the framing
progressText.backgroundColor = .cyan
self.backgroundColor = .systemGreen
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
}
And an example view controller showing it. Each tap anywhere in the view will cycle through several "example" progress strings:
class PVTestViewController: UIViewController {
let progressBar = PVProgressIndicator()
let sampleStrings: [String] = [
"Short",
"Medium Length",
"A Longer Sample String",
"This one should be long enough that it will word wrap onto more than one line.",
"Let's make this sample string\nuse\n4 lines\nof text.",
"If the progress text might be very, very long... so long that it could wrap onto more than four lines (like this), then some additional constraints will need to be set on the elements in the progressBar view.",
]
var sampleIDX: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
progressBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressBar)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain progressBar Top 20-points from top of safe area
progressBar.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// centered horizontally
progressBar.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// let's set the max width to 75% of safe area width
progressBar.widthAnchor.constraint(lessThanOrEqualTo: g.widthAnchor, multiplier: 0.75),
// progressBar sets it's own height
])
updateProgress()
// instruction label
let v = UILabel()
v.text = "Tap anywhere to cycle through the sample progress strings."
v.numberOfLines = 0
v.textAlignment = .center
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func updateProgress() {
progressBar.progressText.text = sampleStrings[sampleIDX % sampleStrings.count]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
sampleIDX += 1
updateProgress()
}
}
When you start, it will look like this:
Each tap cycles to the next sample string...

Change default StackView animation

Forgive me if explanation is not excellent. Basically, the video below shows the standard animation for hiding labels in a stack view. Notice it looks like the labels "slide" and "collapse together".
I still want to hide the labels, but want an animation where the alpha changes but the labels don't "slide". Instead, the labels change alpha and stay in place. Is this possible with stack views?
This is the code I have to animate:
UIView.animate(withDuration: 0.5) {
if self.isExpanded {
self.topLabel.alpha = 1.0
self.bottomLabel.alpha = 1.0
self.topLabel.isHidden = false
self.bottomLabel.isHidden = false
} else {
self.topLabel.alpha = 0.0
self.bottomLabel.alpha = 0.0
self.topLabel.isHidden = true
self.bottomLabel.isHidden = true
}
}
Update 1
It seems that even without a stack view, if I animate the height constraint, you get this "squeeze" effect. Example:
UIView.animate(withDuration: 3.0) {
self.heightConstraint.constant = 20
self.view.layoutIfNeeded()
}
Here are a couple options:
Set .contentMode = .top on the labels. I've never found Apple docs that clearly describe using .contentMode with UILabel, but it works and should work.
Embed the label in a UIView, constrained to the top, with Content Compression Resistance Priority set to .required, less-than-required priority for the bottom constraint, and .clipsToBounds = true on the view.
Example 1 - content mode:
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let botLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// let's add a label and a Switch to toggle the labels .contentMode
let promptView = UIView()
let hStack = UIStackView()
hStack.spacing = 8
let prompt = UILabel()
prompt.text = "Content Mode Top:"
prompt.textAlignment = .right
let sw = UISwitch()
sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
hStack.addArrangedSubview(prompt)
hStack.addArrangedSubview(sw)
hStack.translatesAutoresizingMaskIntoConstraints = false
promptView.addSubview(hStack)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[promptView, stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
promptView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.topAnchor.constraint(equalTo: promptView.bottomAnchor, constant: 0.0),
// center the hStack in the promptView
hStack.centerXAnchor.constraint(equalTo: promptView.centerXAnchor),
hStack.centerYAnchor.constraint(equalTo: promptView.centerYAnchor),
promptView.heightAnchor.constraint(equalTo: hStack.heightAnchor, constant: 16.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func switchChanged(_ sender: UISwitch) {
[topLabel, botLabel].forEach { v in
v.contentMode = sender.isOn ? .top : .left
}
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
Example 2 - label embedded in a UIView:
class TopAlignedLabelView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.addSubview(label)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
// we need bottom anchor to have
// less-than-required Priority
let c = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
c.priority = .required - 1
c.isActive = true
// don't allow label to be compressed
label.setContentCompressionResistancePriority(.required, for: .vertical)
// we need to clip the label
self.clipsToBounds = true
}
}
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let botLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
if let vv = v as? UILabel {
vv.font = .systemFont(ofSize: 24.0, weight: .light)
}
if let vv = v as? TopAlignedLabelView {
vv.label.font = .systemFont(ofSize: 24.0, weight: .light)
}
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.label.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.label.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
Edit
If your goal is to have the Brown label "slide up and cover" both the Blue and Pink labels, with neither of those labels compressing or moving, take a similar approach:
use standard UILabel instead of the TopAlignedLabelView
embed the Blue and Pink labels in their own stack view
embed that stack view in a "container" view
constrain that stack view to be "top-aligned" like we did with the label in the TopAlignedLabelView
The arranged subviews of the "outer" stack view will now be:
Yellow label
"container" view
Brown label
Gray label
and to animate we'll toggle the .alpha and .isHidden on the "container" view instead of the Blue and Pink labels.
I edited the controller class -- give it a try and see if that's the effect you're after.
If it is, I strongly suggest you try to make those changes yourself... if you run into problems, use this example code as a guide:
class StackAnimVC: UIViewController {
let outerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
// create an "inner" stack view
// this will hold topLabel and botLabel
let innerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
return v
}()
// container for the inner stack view
let innerStackContainer: UIView = {
let v = UIView()
v.clipsToBounds = true
return v
}()
// we can use standard UILabels instead of custom views
let topLabel = UILabel()
let botLabel = UILabel()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to inner stack view
innerStackView.addArrangedSubview(topLabel)
innerStackView.addArrangedSubview(botLabel)
// add inner stack view to container
innerStackView.translatesAutoresizingMaskIntoConstraints = false
innerStackContainer.addSubview(innerStackView)
// constraints for inner stack view
// bottom constraint must be less-than-required
// so it doesn't compress when the container compresses
let isvBottom: NSLayoutConstraint = innerStackView.bottomAnchor.constraint(equalTo: innerStackContainer.bottomAnchor, constant: -8.0)
isvBottom.priority = .defaultHigh
NSLayoutConstraint.activate([
innerStackView.topAnchor.constraint(equalTo: innerStackContainer.topAnchor, constant: 0.0),
innerStackView.leadingAnchor.constraint(equalTo: innerStackContainer.leadingAnchor, constant: 0.0),
innerStackView.trailingAnchor.constraint(equalTo: innerStackContainer.trailingAnchor, constant: 0.0),
isvBottom,
])
topLabel.numberOfLines = 0
botLabel.numberOfLines = 0
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
// add views to outer stack view
[headerLabel, innerStackContainer, threeLabel, footerLabel].forEach { v in
outerStackView.addArrangedSubview(v)
}
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[outerStackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
outerStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on inner stack container
self.innerStackContainer.alpha = self.innerStackContainer.isHidden ? 1.0 : 0.0
self.innerStackContainer.isHidden.toggle()
}
}
}
Edit 2
A quick explanation of why this works...
Consider a typical UILabel as a subview of a UIView. We constrain the label to the view on all 4 sides with a little "padding":
aLabel.topAnchor.constraint(equalTo: aView.topAnchor, constant: 8.0),
aLabel.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 8.0),
aLabel.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: -8.0),
aLabel.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: -8.0),
Now we can constrain the view's Top / Leading / Trailing -- but not Bottom or Height -- and the label's intrinsic Height will control the Height of the view.
Pretty basic.
But, if we want to "animate it out of existence," changing the Height of the view will also change the Height of the label, resulting in a "squeeze" effect. We'll also get auto-layout complaints, because the constraints cannot be satisfied.
So, we need to change the .priority of the label's Bottom constraint to allow it to remain at its intrinsic Height, while its superview's Height changes.
Each of these 4 examples uses the same Top / Leading / Trailing constraints... the only difference is what we do with the Bottom constraint:
For Example 1, we don't set any Bottom constraint. So, we never even see its superview and animating the Height of its superview has no effect on the label.
For Example 2, we set the "normal" Bottom constraint, and we see the "squeezing" effect.
For Example 3, we give the label's Bottom constraint .priority = .defaultHigh. The label still controls the Height of its superview... until we activate the superview's Height constraint (of zero). The superview collapses, but we've given auto-layout permission to break the Bottom constraint.
Example 4 is the same as 3, but we've also set .clipsToBounds = true on the container view so the label Height remains constant, but no longer extends outside its superview.
All of that also applies to views in a stack view when setting .isHidden on an arranged subview.
Here's the code that generates that example, if you want to inspect it and play around with the variations:
class DemoVC: UIViewController {
var containerViews: [UIView] = []
var heightConstraints: [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
// create 4 container views, each with a label as a subview
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .systemYellow,
]
colors.forEach { bkgColor in
let thisContainer = UIView()
thisContainer.translatesAutoresizingMaskIntoConstraints = false
let thisLabel = UILabel()
thisLabel.translatesAutoresizingMaskIntoConstraints = false
thisContainer.backgroundColor = bkgColor
thisLabel.backgroundColor = UIColor(red: 0.75, green: 0.9, blue: 1.0, alpha: 1.0)
thisLabel.numberOfLines = 0
//thisLabel.font = .systemFont(ofSize: 20.0, weight: .light)
thisLabel.font = .systemFont(ofSize: 12.0, weight: .light)
thisLabel.text = "We want to animate compressing the \"container\" view vertically, without it squeezing or moving this label."
// add label to container view
thisContainer.addSubview(thisLabel)
// add container view to array
containerViews.append(thisContainer)
// add container view to view
view.addSubview(thisContainer)
NSLayoutConstraint.activate([
// each example gets the label constrained
// Top / Leading / Trailing to its container view
thisLabel.topAnchor.constraint(equalTo: thisContainer.topAnchor, constant: 8.0),
thisLabel.leadingAnchor.constraint(equalTo: thisContainer.leadingAnchor, constant: 8.0),
thisLabel.trailingAnchor.constraint(equalTo: thisContainer.trailingAnchor, constant: -8.0),
// we'll be using different bottom constraints for the examples,
// so don't set it here
//thisLabel.bottomAnchor.constraint(equalTo: thisContainer.bottomAnchor, constant: -8.0),
// each container view gets constrained to the top
thisContainer.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),
])
// setup the container view height constraints, but don't activate them
let hc = thisContainer.heightAnchor.constraint(equalToConstant: 0.0)
// add the constraint to the constraints array
heightConstraints.append(hc)
}
// couple vars to reuse
var prevContainer: UIView!
var aContainer: UIView!
var itsLabel: UIView!
var bc: NSLayoutConstraint!
// -------------------------------------------------------------------
// first example
// we don't add a bottom constraint for the label
// that means we'll never see its container view
// and changing its height constraint won't do anything to the label
// -------------------------------------------------------------------
// second example
aContainer = containerViews[1]
itsLabel = aContainer.subviews.first
// we'll add a "standard" bottom constraint
// so now we see its container view
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.isActive = true
// -------------------------------------------------------------------
// third example
aContainer = containerViews[2]
itsLabel = aContainer.subviews.first
// add the same bottom constraint, but give it a
// less-than-required Priority so it won't "squeeze"
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// -------------------------------------------------------------------
// fourth example
aContainer = containerViews[3]
itsLabel = aContainer.subviews.first
// same less-than-required Priority bottom constraint,
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// we'll also set clipsToBounds on the container view
// so it will "hide / reveal" the label
aContainer.clipsToBounds = true
// now we need to layout the views
// constrain first example leading
aContainer = containerViews[0]
aContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0).isActive = true
prevContainer = aContainer
for i in 1..<containerViews.count {
aContainer = containerViews[i]
aContainer.leadingAnchor.constraint(equalTo: prevContainer.trailingAnchor, constant: 8.0).isActive = true
aContainer.widthAnchor.constraint(equalTo: prevContainer.widthAnchor).isActive = true
prevContainer = aContainer
}
// constrain last example trailing
prevContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0).isActive = true
// and, let's add labels above the 4 examples
for (i, v) in containerViews.enumerated() {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Example \(i + 1)"
label.font = .systemFont(ofSize: 14.0, weight: .light)
view.addSubview(label)
NSLayoutConstraint.activate([
label.bottomAnchor.constraint(equalTo: v.topAnchor, constant: -4.0),
label.centerXAnchor.constraint(equalTo: v.centerXAnchor),
])
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
heightConstraints.forEach { c in
c.isActive = !c.isActive
}
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
}
}

Multiple UIStackViews inside a custom UITableViewCell in Custom Cell without Storyboard not working

I am currently building out a screen in my app which is basically a long UITableView containing 3 Sections, each with different amounts of unique custom cells. Setting up The tableview works fine, I added some random text in the cells to make sure every cell is correctly called and positioned. I have completely deletet my storyboard from my project because it would lead to problems later because of reasons. So I can't do anything via storyboard.
Next step is to build the custom cells. Some of those are fairly complex for me as a beginner. This is one of my cells:
I want to split the cell in multiple UIStackViews, one for the picture and the name and one for the stats on the right side which in itself will contain two stackviews containing each of the two rows of stats. Each of these could then contain another embedded stackview with the two uiLabels inside, the number and the description. Above all that is a toggle button.
I can't seem to grasp how to define all this. As I said, I defined the Tableview and am calling the right cells in my cellForRowAt as shown here for example:
if indexPath.row == 0 && indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: StatsOverViewCell.identifier, for: indexPath) as! StatsOverViewCell
cell.configure()
return cell
} else if ...
I have created files for each cell, one of them being StatsOverViewCell.
In this file, I have an Identifier with the same name as the class.
I have also added the configure function I am calling from my tableview, the layoutSubviews function which I use to layout the views inside of the cell and I have initialized every label and image I need. I have trimmed the file down to a few examples to save you some time:
class StatsOverViewCell: UITableViewCell {
//Set identifier to be able to call it later on
static let identifier = "StatsOverViewCell"
let myProfileStackView = UIStackView()
let myImageView = UIImageView()
let myName = UILabel()
let myGenderAndAge = UILabel()
let myStatsStackView = UIStackView()
let oneView = UIStackView()
let oneStat = UILabel()
let oneLabel = UILabel()
let twoStackView = UIStackView()
let twoStat = UILabel()
let twoLabel = UILabel()
//Do this for each of the labels I have in the stats
public func configure() {
myImageView.image = UIImage(named: "person-icon")
myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
myImageView.contentMode = .scaleAspectFill
myName.text = "Name."
myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
myName.textAlignment = .center
//Add the Name label to the stackview
myProfileStackView.addArrangedSubview(myName)
myProfileStackView.addArrangedSubview(myImageView)
myName.centerXAnchor.constraint(equalTo: myProfileStackView.centerXAnchor).isActive = true
oneStat.text = "5.187"
oneStat.font = UIFont(name: "montserrat", size: 18)
oneLabel.text = "Text"
oneLabel.font = UIFont(name: "montserrat", size: 14)
}
//Layout in the cell
override func layoutSubviews() {
super.layoutSubviews()
contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
contentView.layer.borderWidth = 1
//Stackview
contentView.addSubview(myProfileStackView)
myProfileStackView.axis = .vertical
myProfileStackView.distribution = .equalSpacing
myProfileStackView.spacing = 3.5
myProfileStackView.backgroundColor = .red
myProfileStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 23).isActive = true
myProfileStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 76).isActive = true
}
As you can see, I am adding all arrangedsubviews to the stackview in the configure method which I call when creating the cell in the tableview. I then set the stackviews constraints inside the layoutsubviews. I am not getting any errors or anything. But the cell shows up completely empty.
I feel like I am forgetting something or I am not understanding how to create cells with uistackviews. Where should I create the stackviews, where should I add the arrangedsubviews to this stackview and what do I do in the LayoutSubviews?
I would be very thankful for any insights.
Thanks for your time!
You're doing a few things wrong...
your UI elements should be created and configured in init, not in configure() or layoutSubviews()
you need complete constraints to give your elements the proper layout
Take a look at the changes I made to your cell class. It should get you on your way:
class StatsOverViewCell: UITableViewCell {
//Set identifier to be able to call it later on
static let identifier = "StatsOverViewCell"
let myProfileStackView = UIStackView()
let myImageView = UIImageView()
let myName = UILabel()
let myGenderAndAge = UILabel()
let myStatsStackView = UIStackView()
let oneView = UIStackView()
let oneStat = UILabel()
let oneLabel = UILabel()
let twoStackView = UIStackView()
let twoStat = UILabel()
let twoLabel = UILabel()
//Do this for each of the labels I have in the stats
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
myImageView.image = UIImage(named: "person-icon")
// frame doesn't matter - stack view arrangedSubvies automatically
// set .translatesAutoresizingMaskIntoConstraints = false
//myName.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
myImageView.contentMode = .scaleAspectFill
myName.text = "Name."
myName.textAlignment = .center
//Add the Name label to the stackview
myProfileStackView.addArrangedSubview(myName)
myProfileStackView.addArrangedSubview(myImageView)
// no need for this
//myName.centerXAnchor.constraint(equalTo: myProfileStackView.centerXAnchor).isActive = true
oneStat.text = "5.187"
oneStat.font = UIFont(name: "montserrat", size: 18)
oneLabel.text = "Text"
oneLabel.font = UIFont(name: "montserrat", size: 14)
contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
contentView.layer.borderWidth = 1
//Stackview
contentView.addSubview(myProfileStackView)
myProfileStackView.axis = .vertical
// no need for equalSpacing if you're explicitly setting the spacing
//myProfileStackView.distribution = .equalSpacing
myProfileStackView.spacing = 3.5
myProfileStackView.backgroundColor = .red
// stack view needs .translatesAutoresizingMaskIntoConstraints = false
myProfileStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// stack view leading 23-pts from contentView leading
myProfileStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 23),
// stack view top 76-pts from contentView top
myProfileStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 76),
// need something to set the contentView height
// stack view bottom 8-pts from contentView bottom
myProfileStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
// set imageView width and height
myImageView.widthAnchor.constraint(equalToConstant: 100.0),
myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),
])
}
public func configure() {
// here you would set the properties of your elements, such as
// label text
// imageView image
// colors
// etc
}
}
Edit
Here's an example cell class that comes close to the layout in the image you posted.
Note that there are very few constraints needed:
NSLayoutConstraint.activate([
// role element 12-pts from top
myRoleElement.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
// centered horizontally
myRoleElement.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// it will probably be using intrinsic height and width, but for demo purposes
myRoleElement.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.4),
myRoleElement.heightAnchor.constraint(equalToConstant: 40.0),
// stack view 24-pts on each side
hStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
// stack view 20-pts on bottom
hStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
// stack view top 20-pts from Role element bottom
hStack.topAnchor.constraint(equalTo: myRoleElement.bottomAnchor, constant: 20),
// set imageView width and height
myImageView.widthAnchor.constraint(equalToConstant: 100.0),
myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),
// we want the two "column" stack views to be equal widths
hStack.arrangedSubviews[1].widthAnchor.constraint(equalTo: hStack.arrangedSubviews[2].widthAnchor),
])
Here's the full cell class, including an example "UserStruct" ... you will, of course, want to tweak the fonts / sizes, spacing, etc:
// sample struct for user data
struct UserStruct {
var profilePicName: String = ""
var name: String = ""
var gender: String = ""
var age: Int = 0
var statValues: [String] = []
}
class StatsOverViewCell: UITableViewCell {
//Set identifier to be able to call it later on
static let identifier = "StatsOverViewCell"
// whatever your "role" element is...
let myRoleElement = UILabel()
let myImageView = UIImageView()
let myName = UILabel()
let myGenderAndAge = UILabel()
var statValueLabels: [UILabel] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
// create 6 Value and 6 text labels
// assuming you have 6 "Text" strings, but for now
// we'll use "Text A", "Text B", etc
let tmp: [String] = [
"A", "B", "C",
"D", "E", "F",
]
var statTextLabels: [UILabel] = []
for i in 0..<6 {
var lb = UILabel()
lb.font = UIFont.systemFont(ofSize: 16, weight: .regular)
lb.textAlignment = .center
lb.textColor = .white
lb.text = "0"
statValueLabels.append(lb)
lb = UILabel()
lb.font = UIFont.systemFont(ofSize: 13, weight: .regular)
lb.textAlignment = .center
lb.textColor = .lightGray
lb.text = "Text \(tmp[i])"
statTextLabels.append(lb)
}
// name and Gender/Age label properties
myName.textAlignment = .center
myGenderAndAge.textAlignment = .center
myName.font = UIFont.systemFont(ofSize: 15, weight: .regular)
myGenderAndAge.font = UIFont.systemFont(ofSize: 15, weight: .regular)
myName.textColor = .white
myGenderAndAge.textColor = .white
// placeholder text
myName.text = "Name"
myGenderAndAge.text = "(F, 26)"
myImageView.contentMode = .scaleAspectFill
// create the "Profile" stack view
let myProfileStackView = UIStackView()
myProfileStackView.axis = .vertical
myProfileStackView.spacing = 2
//Add imageView, name and gender/age labels to the profile stackview
myProfileStackView.addArrangedSubview(myImageView)
myProfileStackView.addArrangedSubview(myName)
myProfileStackView.addArrangedSubview(myGenderAndAge)
// create horizontal stack view to hold
// Profile stack + two "column" stack views
let hStack = UIStackView()
// add Profile stack view
hStack.addArrangedSubview(myProfileStackView)
var j: Int = 0
// create two "column" stack views
// each with three "label pair" stack views
for _ in 0..<2 {
let columnStack = UIStackView()
columnStack.axis = .vertical
columnStack.distribution = .equalSpacing
for _ in 0..<3 {
let pairStack = UIStackView()
pairStack.axis = .vertical
pairStack.spacing = 4
pairStack.addArrangedSubview(statValueLabels[j])
pairStack.addArrangedSubview(statTextLabels[j])
columnStack.addArrangedSubview(pairStack)
j += 1
}
hStack.addArrangedSubview(columnStack)
}
// whatever your "Roles" element is...
// here, we'll simulate it with a label
myRoleElement.text = "Role 1 / Role 2"
myRoleElement.textAlignment = .center
myRoleElement.textColor = .white
myRoleElement.backgroundColor = .systemTeal
myRoleElement.layer.cornerRadius = 8
myRoleElement.layer.borderWidth = 1
myRoleElement.layer.borderColor = UIColor.white.cgColor
myRoleElement.layer.masksToBounds = true
// add Role element and horizontal stack view to contentView
contentView.addSubview(myRoleElement)
contentView.addSubview(hStack)
myRoleElement.translatesAutoresizingMaskIntoConstraints = false
hStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// role element 12-pts from top
myRoleElement.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
// centered horizontally
myRoleElement.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// it will probably be using intrinsic height and width, but for demo purposes
myRoleElement.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.4),
myRoleElement.heightAnchor.constraint(equalToConstant: 40.0),
// stack view 24-pts on each side
hStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
// stack view 20-pts on bottom
hStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
// stack view top 20-pts from Role element bottom
hStack.topAnchor.constraint(equalTo: myRoleElement.bottomAnchor, constant: 20),
// set imageView width and height
myImageView.widthAnchor.constraint(equalToConstant: 100.0),
myImageView.heightAnchor.constraint(equalTo: myImageView.widthAnchor),
// we want the two "column" stack views to be equal widths
hStack.arrangedSubviews[1].widthAnchor.constraint(equalTo: hStack.arrangedSubviews[2].widthAnchor),
])
//contentView.backgroundColor = Utilities.hexStringToUIColor(hex: "#3F454B")
contentView.backgroundColor = UIColor(red: 0x3f / 255.0, green: 0x45 / 255.0, blue: 0x4b / 255.0, alpha: 1.0)
contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor.lightGray.cgColor
// since we're setting the image view to explicit 100x100 size,
// we can make it round here
myImageView.layer.cornerRadius = 50
myImageView.layer.masksToBounds = true
}
public func configure(_ user: UserStruct) {
// here you would set the properties of your elements
// however you're getting your profile image
var img: UIImage!
if !user.profilePicName.isEmpty {
img = UIImage(named: user.profilePicName)
}
if img == nil {
img = UIImage(named: "person-icon")
}
if img != nil {
myImageView.image = img
}
myName.text = user.name
myGenderAndAge.text = "(\(user.gender), \(user.age))"
// probably want error checking to make sure we have 6 values
if user.statValues.count == 6 {
for (lbl, s) in zip(statValueLabels, user.statValues) {
lbl.text = s
}
}
}
}
and a sample table view controller:
class UserStatsTableViewController: UITableViewController {
var myData: [UserStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(StatsOverViewCell.self, forCellReuseIdentifier: StatsOverViewCell.identifier)
// generate some sample data
// I'm using Female "pro1" and Male "pro2" images
for i in 0..<10 {
var user = UserStruct(profilePicName: i % 2 == 0 ? "pro2" : "pro1",
name: "Name \(i)",
gender: i % 2 == 0 ? "F" : "M",
age: Int.random(in: 21...65))
var vals: [String] = []
for _ in 0..<6 {
let v = Int.random(in: 100..<1000)
vals.append("\(v)")
}
user.statValues = vals
myData.append(user)
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StatsOverViewCell.identifier, for: indexPath) as! StatsOverViewCell
let user = myData[indexPath.row]
cell.configure(user)
return cell
}
}
This is how it looks at run-time:

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