I am laying out a similar UIStackView to Twitter's author layout as per below;
However, I am not getting the desired effect from setting the various priorities;
setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .horizontal)
The UIStackView contains the below items;
Display Name UILabel
Verified Badge UIImageView
Handle UILabel
Timestamp UILabel
The functionality I am looking for is the below;
The Timestamp and Verified Badge must not shrink at any time.
The Timestamp must take up the remaining space if the UIStackView doesn't fill the width
The Handle will shrink first
The Display Name will shrink last
How can I achieve this?
You can follow your "functionality I am looking for" list as you've written it...
The Timestamp and Verified Badge must not shrink at any time.
// don't let Timestamp compress
timeStampLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// don't let "Dot" compress
dotLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// badge image view is square (1:1 ratio)
// Width Anchor prevents both compression and expansion
badgeImageView.widthAnchor.constraint(equalTo: badgeImageView.heightAnchor).isActive = true
The Timestamp must take up the remaining space if the UIStackView doesn't fill the width
// don't let Display Name, Handle or Dot expand Horizontally
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
handleLabel.setContentHuggingPriority(.required, for: .horizontal)
dotLabel.setContentHuggingPriority(.required, for: .horizontal)
The Handle will shrink first
// Handle shrink first
handleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
The Display Name will shrink last
// Display Name shrink next
displayNameLabel.setContentCompressionResistancePriority(.defaultLow + 1, for: .horizontal)
You didn't specify, so...
// Handle *could* to shrink to "no width"
// so use a min-Width to show at least a char or two
// if you want to allow it to disappear, comment out this line
handleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 24.0).isActive = true
Here's a complete example -- it has two buttons... one to cycle through some sample data, and one to toggle background colors on the labels to see their frames. The stackView is added to the main view, but it will work the same when used in (what I assume is) your cell:
class TwitLineVC: UIViewController {
let displayNameLabel = UILabel()
let badgeImageView = UIImageView()
let handleLabel = UILabel()
let dotLabel = UILabel()
let timeStampLabel = UILabel()
let sampleData: [[String]] = [
["Stack Overflow", "#StackOverflow", "Nov 13"],
["Longer Display Name", "#LongerHandle", "Nov 14"],
["Much Longer Display Name", "#ThisHandleWillCompress", "Nov 15"],
["Much Longer Display Name Will Also Compress", "#ThisHandleWillCompress", "Nov 16"],
]
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
guard let img = UIImage(named: "twcheck") else {
fatalError("Could not load image!")
}
badgeImageView.image = img
let stackView = UIStackView()
stackView.spacing = 4
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
// don't let Display Name, Handle or Dot expand Horizontally
displayNameLabel.setContentHuggingPriority(.required, for: .horizontal)
handleLabel.setContentHuggingPriority(.required, for: .horizontal)
dotLabel.setContentHuggingPriority(.required, for: .horizontal)
// don't let Timestamp compress
timeStampLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// don't let "Dot" compress
dotLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// badge image view is square (1:1 ratio)
// Width Anchor prevents both compression and expansion
badgeImageView.widthAnchor.constraint(equalTo: badgeImageView.heightAnchor).isActive = true
// Handle shrink first
handleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
// Display Name shrink next
displayNameLabel.setContentCompressionResistancePriority(.defaultLow + 1, for: .horizontal)
// Handle *could* to shrink to "no width"
// so use a min-Width to show at least a char or two
// if you want to allow it to disappear, comment out this line
handleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 24.0).isActive = true
// use Display Name label height to control stack view height
displayNameLabel.setContentHuggingPriority(.required, for: .vertical)
// add to stack view
stackView.addArrangedSubview(displayNameLabel)
stackView.addArrangedSubview(badgeImageView)
stackView.addArrangedSubview(handleLabel)
stackView.addArrangedSubview(dotLabel)
stackView.addArrangedSubview(timeStampLabel)
displayNameLabel.textColor = .white
handleLabel.textColor = .lightGray
dotLabel.textColor = .lightGray
timeStampLabel.textColor = .lightGray
let fSize: CGFloat = 12.0
displayNameLabel.font = .systemFont(ofSize: fSize, weight: .bold)
handleLabel.font = .systemFont(ofSize: fSize, weight: .regular)
dotLabel.font = handleLabel.font
timeStampLabel.font = handleLabel.font
// this never changes
dotLabel.text = "•"
// a button to cycle through sample data
let btn1 = UIButton(type: .system)
btn1.setTitle("Next Data Set", for: [])
btn1.addTarget(self, action: #selector(updateData(_:)), for: .touchUpInside)
btn1.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn1)
// a button to toggle background colors
let btn2 = UIButton(type: .system)
btn2.setTitle("Toggle Colors", for: [])
btn2.addTarget(self, action: #selector(toggleColors(_:)), for: .touchUpInside)
btn2.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn2)
NSLayoutConstraint.activate([
btn1.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 40.0),
btn1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btn2.topAnchor.constraint(equalTo: btn1.bottomAnchor, constant: 20.0),
btn2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
// fill with the first data set
updateData(nil)
}
#objc func updateData(_ sender: Any?) {
displayNameLabel.text = sampleData[idx % sampleData.count][0]
handleLabel.text = sampleData[idx % sampleData.count][1]
timeStampLabel.text = sampleData[idx % sampleData.count][2]
idx += 1
}
#objc func toggleColors(_ sender: Any?) {
if displayNameLabel.backgroundColor == .clear {
displayNameLabel.backgroundColor = .systemGreen
handleLabel.backgroundColor = .systemBlue
dotLabel.backgroundColor = .systemYellow
timeStampLabel.backgroundColor = .systemRed
} else {
displayNameLabel.backgroundColor = .clear
handleLabel.backgroundColor = .clear
dotLabel.backgroundColor = .clear
timeStampLabel.backgroundColor = .clear
}
}
}
Related
I am using 2 UIButton in same y position. The one on the left is shorter in text than the one on the right:
example image
I am adding
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.5
for each button. The problem is that one button font size get smaller than the other. For example, they are both with font size 18, one might get 12 and the other will stay 18. I want them both to be 15 (the larger possible keeping both texts visible)
The text inserted to the buttons changes so i can't use aspect ratio.
Also, i don't want to use the font of the button with the smaller text because the difference is sometimes to high.
You can do it programmatically with stack view, declare your buttons under your controller class:
let myButton: UIButton = {
let b = UIButton()
b.backgroundColor = .white
b.setTitle("Short button", for: .normal)
b.setTitleColor(.black, for: .normal)
b.titleLabel?.font = .systemFont(ofSize: 17, weight: .regular)
b.layer.cornerRadius = 12
b.clipsToBounds = true
b.titleLabel?.adjustsFontSizeToFitWidth = true
b.titleLabel?.minimumScaleFactor = 0.5
b.translatesAutoresizingMaskIntoConstraints = false
return b
}()
let myButton2: UIButton = {
let b = UIButton()
b.backgroundColor = .white
b.setTitle("Much Much Much longer button", for: .normal)
b.setTitleColor(.black, for: .normal)
b.titleLabel?.font = .systemFont(ofSize: 17, weight: .regular)
b.layer.cornerRadius = 12
b.clipsToBounds = true
b.titleLabel?.adjustsFontSizeToFitWidth = true
b.titleLabel?.minimumScaleFactor = 0.5
b.translatesAutoresizingMaskIntoConstraints = false
return b
}()
Now in viewDidLoad set stackView and constraints (I set 6 for space, but you can set your prefer space, look at the comment to do it):
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
let stackView = UIStackView(arrangedSubviews: [myButton, myButton2])
stackView.distribution = .fillProportionally
stackView.spacing = 6 // change space betwenn buttons
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
This is the result shortButton/shortButton and shortButton/longerButton, the width of the buttons changes dynamically according to the text:
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:
Really struggling to constrain my buttons.
All I want is do setup my 2 buttons to have a height of 35 and their width should be whatever they need. Right now it looks like this (left & right buttons):
This is how I set them up:
let communityButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(systemName: "person.3.fill"), for: .normal)
v.tintColor = UIColor.darkCustom
v.imageView?.contentMode = .scaleAspectFill
v.contentHorizontalAlignment = .fill
v.contentVerticalAlignment = .fill
v.translatesAutoresizingMaskIntoConstraints = false
v.addTarget(self, action: #selector(communityButtonTapped), for: .touchUpInside)
return v
}()
let profileButton: UIButton = {
let v = UIButton()
v.setImage(UIImage(systemName: "person.fill"), for: .normal)
v.tintColor = UIColor.darkCustom
v.imageView?.contentMode = .scaleAspectFill
v.contentHorizontalAlignment = .fill
v.contentVerticalAlignment = .fill
v.translatesAutoresizingMaskIntoConstraints = false
v.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside)
return v
}()
Constraints:
//contrain communityButton
communityButton.centerYAnchor.constraint(equalTo: bottomBar.centerYAnchor),
communityButton.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor, constant: -view.frame.width/3.5),
communityButton.heightAnchor.constraint(equalToConstant: 35),
// constrain profileButton
profileButton.centerYAnchor.constraint(equalTo: bottomBar.centerYAnchor),
profileButton.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor, constant: view.frame.width/3.5),
profileButton.heightAnchor.constraint(equalToConstant: 35),
What is the right way to constrain here?
You might need to let autoLayout know that you want width to be flexible. You can do that by adding this line on both of your button definitions: -
// Replace "myButton" with your buttons name in your case "v"
myButton.autoresizingMask = [.flexibleWidth]
Edits: -
Since you are using system images, if you want your button to look bigger then you have to increase the font size on your image configuration. Edit your images by adding configuration with your preferred font sizes.
For example: -
let communityButton: UIButton = {
let v = UIButton()
//----
// NB: - Change the font size for bigger icons and viceversa
let imageSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 50, weight: .regular, scale: .large)
v.setImage(UIImage(systemName: "person.3.fill", withConfiguration: imageSymbolConfiguration), for: .normal)
// -----
return v
}()
let profileButton: UIButton = {
let v = UIButton()
// ----
// NB: - Change the font size for bigger icons and viceversa
let imageSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 50, weight: .regular, scale: .large)
v.setImage(UIImage(systemName: "person.fill", withConfiguration: imageSymbolConfiguration), for: .normal)
// ----
return v
}()
Explanation:-
From the official documentation
"Symbol image configuration objects include details such as the point size, scale, text style, weight, and font to apply to your symbol image. The system uses these details to determine which variant of the image to use and how to scale or style the image."
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
}
}
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
}
}