I create UIStackViews programmatically and add them to parent UIStackView, which is created in Storyboard. Child stack views are horizontal with 2 labels. I need to fix width of second UILabel and make the first UILabel fill the rest space.
Now I have this:
And I want this:
My code for generating children stack views:
#IBOutlet weak var parentStackView: UIStackView!
func addStackViewsToParentStackView(params: [String: Float]) {
for (name, value) in params {
let parameterNameLabel = UILabel() // first label
parameterNameLabel.text = name
let parameterValueLabel = UILabel() // second label
parameterValueLabel.text = value.description
parameterValueLabel.frame.size.width = 80.0 // I've tried to fix width, but it does't help
let childStackView = UIStackView(arrangedSubviews: [parameterNameLabel, parameterValueLabel])
childStackView.axis = .Horizontal
childStackView.distribution = .FillProportionally
childStackView.alignment = .Fill
childStackView.spacing = 5
childStackView.translatesAutoresizingMaskIntoConstraints = true
parentStackView.addArrangedSubview(childStackView)
}
}
Thanks for any help!
Just put width constraints on the labels.
parameterValueLabel.widthAnchor.constraint(equalToConstant: 80).isActive = true
Related
I'm designing a UI in Swift 5 programatically. Within the superview, I've added a subview called viewLeft1 with the type UIStackView. Inside viewLeft1's declaration, I've created a UILabel with some default attributes.
The declaration of viewLeft1
let viewLeft1: UIStackView = {
let stackView = UIStackView()
let lbl = UILabel()
stackView.addSubview(lbl)
lbl.text = "--"
return stackView
}()
An example of a function that would change the text attribute of lbl
func updateLabel1() {
// Change the lbl.text to "302"
}
The problem is that I don't know how to access lbl because it was defined within viewLeft1.
How could I achieve this? Any help is appreciated! Thanks.
I would rather add another property for the label:
let label: UILabel = {
let lbl = UILabel()
lbl.text = "--"
return lbl
}()
let viewLeft1: UIStackView = {
let stackView = UIStackView()
// configure other properties of stackView, such as constraints,
// but don't add viewLeft1 to it here
return stackView
}()
And then in the initialiser of the view, where you add viewLeft1 to self, also add label to viewLeft1:
self.addSubview(viewLeft1)
viewLeft1.addArrangedSubview(label)
I have a view controller created in the storyboard and the it contains (check image for reference)
ScrollerView
a. StackViewA (image: green)
i. LabelA
ii. LabelB
b. StackViewB (image: green)
i. LabelC
ii. LabelD
I am fetching data from the API and am able to show that data in those labels.
Now, the 3rd set of data that I am fetching is dynamic, meaning it can be 2 more StackView (image: red) under the second StackView or 3 more etc.
I am guessing that I have the add that StackView programmatically in the controller inside the loop so that is created according to the loop.
Currently my 3rd StackView is also created in the storyboard and therefore it is showing only the last data from 3rd set after looping through them.
How do I solve that?
More specifically:
How can I add a StackView inside the ScrollerView created in the storyboard.
How do I contains it to position itself below the 2nd StackView also created in the storyboard.
Update
class InfoDetailsViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var infoStack: UIStackView!
#IBOutlet weak var mainScroll: UIScrollView!
static var apiResp: APIDetailsResponse.APIReturn?
let infos: [APIDetailsResponse.Info] = (APIDetailsViewController.apiResp?.content?.infos)!
override func viewDidLoad() {
super.viewDidLoad()
infoStack.spacing = 25.0
for info in infos {
let addInfoTitle = UILabel()
addInfoTitle.font = .preferredFont(forTextStyle: .body)
addInfoTitle.backgroundColor = .orange
addInfoTitle.translatesAutoresizingMaskIntoConstraints = false
let addInfoContent = UITextView()
addInfoContent.font = .preferredFont(forTextStyle: .body)
addInfoContent.backgroundColor = .green
addInfoContent.translatesAutoresizingMaskIntoConstraints = false
addInfoTitle.text = "\(String(describing: (info.info_title)!))"
let htmlString = "\(String(describing: (info.information)!))"
// works even without <html><body> </body></html> tags, BTW
let data = htmlString.data(using: String.Encoding.unicode)!
let attrStr = try? NSAttributedString(
data: data,
options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil)
addInfoContent.attributedText = attrStr
let childStackView = UIStackView(arrangedSubviews: [addInfoTitle, addInfoContent])
childStackView.alignment = .fill
childStackView.axis = .vertical
childStackView.distribution = .fill
childStackView.spacing = 5.0
childStackView.translatesAutoresizingMaskIntoConstraints = false
infoStack.addArrangedSubview(childStackView)
}
}
Currently I have this. What is happening now is no matter how many data the array is returning, I am always getting title and content for the first one and only title for each consecutive data.
So the best solution is when you create your scrollview add a stack view inside it. And whenever you are creating stack view dynamically add under that stack view which is inside scrollview.
So in this case you new stack view will gets stacked under your outer stack view in proper manner.
ScrollView -> 1 Stackview -> Multiple Dynamic stack views in loop
lets say you already have stack view named ParentStackview from your storyboard. Then follow these steps
lazy var childStackView: UIStackView = {
let stackView = UIStackView()
stackView.alignment = .center
stackView.axis = .vertical
stackView.distribution = .equalCentering
stackView.spacing = 30.0
return stackView
}()
public func viewDidLoad() {
ParentStackview.alignment = .center
ParentStackview.axis = .vertical
ParentStackview.distribution = .equalCentering
ParentStackview.spacing = 10.0
for eachChild in data {
ParentStackView.addArrangedSubview(childStackView)
childStackView.translatesAutoresizingMaskIntoConstraints = false
childStackView.widthAnchor.constraint(equalTo: securityScrollView.widthAnchor)
//set height too
// this below function you can use to add eachChild data to you child stackview
configureStackview(childstackView, eachChild)
}
}
Enjoy!
I have to UILabel with dynamic height. I want to set it superview height equal to max of UILabel heights.
class ComponentCell: UIView {
private lazy var leftRow: UILabel = UILabel()
private lazy var rightRow: UILabel = UILabel()
init(leftValue: String, rightValue: String) {
super.init(frame: .zero)
leftRow.backgroundColor = .red
leftRow.numberOfLines = 0
leftRow.lineBreakMode = .byWordWrapping
leftRow.text = leftValue
rightRow.text = rightValue
rightRow.backgroundColor = .yellow
rightRow.numberOfLines = 0
rightRow.lineBreakMode = .byWordWrapping
self.addSubview(self.leftRow)
self.addSubview(self.rightRow)
leftRow.sizeToFit()
rightRow.sizeToFit()
leftRow.setContentHuggingPriority(.required, for: .vertical)
rightRow.setContentHuggingPriority(.required, for: .vertical)
self.translatesAutoresizingMaskIntoConstraints = false
self.leftRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.rightRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.right.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.layoutIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If I set leftRow.botton.equalTo(superview.bottom) and rightRow.botton.equalTo(superview.bottom) it's working. But I think is not a good way. And I don't understand why setContentHuggingPriority not helped me to solve this problem.
Content Hugging
Content hugging leads more likely to squeeze your labels. What you want is the height of the labels to be more respected. So you'd rather use compression resistance priority. However you actually need neither of those.
Layout Constraints
Since you're setting your constraints programatically, you'll need to set translatesAutoresizingMaskIntoConstraints to false for your labels as well:
leftRow.translatesAutoresizingMaskIntoConstraints = false
rightRow.translatesAutoresizingMaskIntoConstraints = false
The bottom constraint is actually a good start, but you don't want to fit the height of the smaller label unnecessarily to the height of the bigger label. So you would want to add a constraint that is "less than or equal to the bottom anchor":
make.bottom.lessThanOrEqualTo(self.snp.bottom)
Lazy Variables
If you want to use lazy variables you'll have to change the way there being initialized. The way you've written it, it initializes the variables right away when initializing the class. But you only want them to be initialized when they're used the first time. For that you need to write it like this:
private lazy var leftRow: UILabel = {
return UILabel()
}()
private lazy var rightRow: UILabel = {
return UILabel()
}()
However in your case you don't need lazy loading, so you can initialize them directly:
private let leftRow = UILabel()
private let rightRow = UILabel()
Other
Since you're using layout constraints, you don't need to call sizeToFit on the labels. It doesn't do anything.
Calling layoutIfNeeded() within the init doesn't do anything either since it will be called anyway once you add ComponentCell as a subview to another view.
I'm trying to create a table view cell prototype (similar to one below) programmatically.
The designed the cell with two stack views,
a) a vertical stack view to contain the text labels and,
b) a horizontal stack view to contain the image view & vertical stack view
I create the required views, stuff it in stack view, and pin stack view to table cell's contentView in the init() of tableviewcell.
And from cellForItemAtIndexPath I call configureCell() to populate data of the cell.
My init() looks like this
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
textStackView = UIStackView(arrangedSubviews: [priorityNameLabel, descriptionLabel])
textStackView.axis = .vertical
textStackView.alignment = .leading
textStackView.distribution = .fill
textStackView.spacing = 5
containerStackView = UIStackView(arrangedSubviews: [priorityImageView, textStackView])
containerStackView.axis = .horizontal
containerStackView.alignment = .center
containerStackView.spacing = 5
containerStackView.distribution = .fill
contentView.addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
pinContainerToSuperview()
}
func pinContainerToSuperview() {
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).activate()
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor).activate()
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).activate()
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).activate()
}
In my view controller, I set tableView rowHeight to automaticDimension and estimated height to some approx. value. When I run the code, all I'm getting is,
The narrow horizontal lines on the top of the image are my tableview cells (My data count is 3 in this case). I couldn't figure out the problem. Can someone point out what's going wrong here?
EDIT 1:
These are the instance members of my TableViewCell class
var containerStackView: UIStackView!
var textStackView: UIStackView!
var priorityImageView: UIImageView! {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}
var priorityNameLabel: UILabel! {
let label = UILabel()
return label
}
var descriptionLabel: UILabel! {
let label = UILabel()
return label
}
You have your labels and image view as computed properties - this means that every time you access them a new instance is created. This is bad. This basically means that when you set them as arranged subvies of your stack views and then try to configure them later, you work on different objects.
Simplest solution would be to create your labels and image view the way you create your stack views in init.
I want 2 labels (say leftLabel, rightLabel) and place them horizontally such that leftLabel stretches and rightLabel just fits single character icon (say, ">"). Thus both labels layout justified. Like this...
This is the code I have -
class StackViewController: UIViewController {
/// Main vertical outer/container stack view that pins its edges to this view in storyboard (i.e. full screen)
#IBOutlet weak private var containerStackView: UIStackView!
private var leftLabel: UILabel = {
let leftLabel = UILabel(frame: .zero)
leftLabel.font = .preferredFont(forTextStyle: .body)
leftLabel.numberOfLines = 0 // no text truncation, allows wrap
leftLabel.backgroundColor = .orange
return leftLabel
}()
private var rightLabel: UILabel = {
let rightLabel = UILabel(frame: .zero)
rightLabel.font = .preferredFont(forTextStyle: .body)
// Set CHCR as high so that label sizes itself to fit the text
rightLabel.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .horizontal)
rightLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh, for: .horizontal)
rightLabel.backgroundColor = .green
return rightLabel
}()
override func viewDidLoad() {
super.viewDidLoad()
prepareAndLoadSubViews()
// Note, the text required to be set in viewDidAppear, not viewDidLoad, otherwise rightLabel stretches to fill!!
leftLabel.text = "This is left label text that may go in multiple lines"
rightLabel.text = ">" // Always a single character
}
/// Dynamically creates a horizontal stack view, with 2 labels, in the container stack view
private func prepareAndLoadSubViews() {
/// Prepare the horizontal label stack view and add the 2 labels
let labelStackView = UIStackView(arrangedSubviews: [leftLabel, rightLabel])
labelStackView.axis = .horizontal
labelStackView.distribution = .fillProportionally
labelStackView.alignment = .top
containerStackView.addArrangedSubview(labelStackView)
containerStackView.addArrangedSubview(UIView())
}
}
Which gives below result (i.e. leftLabel width is 0 in view debugger) -
NOTE: If I move text set code in viewDidAppear then it works fine.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Note, the text required to be set in viewDidAppear, not viewDidLoad, otherwise rightLabel stretches to fill!!
leftLabel.text = "This is left label text that may go in multiple lines"
rightLabel.text = ">" // Always a single character
}
Why?
And, can we set content hugging/ compression resistance priorities before viewDidLoad?
I played around with your code quite a bit but I was not able to make it work either. I think this is a bug that occurs when you add a UIStackView to another UIStackView. When you only have one UIStackView your code works fine.
So I cannot offer a fix for your case but IMHO you shouldn't really need to use a UIStackView for your 2 labels at all. UIStackView is great if you have multiple arranged subviews that you hide and show and need to be arranged automatically. For just two "static" labels I think it is a bit of an overkill.
You can achieve what you are after by adding your two labels to a UIView and then set layout constraints to the labels. It's really easy:
class StackViewController: UIViewController {
#IBOutlet weak var containerStackView: UIStackView!
private var leftLabel: UILabel = {
let leftLabel = UILabel(frame: .zero)
leftLabel.font = .preferredFont(forTextStyle: .body)
leftLabel.numberOfLines = 0
leftLabel.backgroundColor = .orange
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.numberOfLines = 0
return leftLabel
}()
private var rightLabel: UILabel = {
let rightLabel = UILabel(frame: .zero)
rightLabel.font = .preferredFont(forTextStyle: .body)
rightLabel.setContentHuggingPriority(UILayoutPriority.required, for: .horizontal)
rightLabel.backgroundColor = .green
rightLabel.translatesAutoresizingMaskIntoConstraints = false
return rightLabel
}()
override func viewDidLoad() {
super.viewDidLoad()
prepareAndLoadSubViews()
leftLabel.text = "This is left label text that may go in multiple lines"
rightLabel.text = ">"
}
private func prepareAndLoadSubViews() {
let labelContainerView = UIView()
labelContainerView.addSubview(leftLabel)
labelContainerView.addSubview(rightLabel)
NSLayoutConstraint.activate([
leftLabel.leadingAnchor.constraint(equalTo: labelContainerView.leadingAnchor),
leftLabel.topAnchor.constraint(equalTo: labelContainerView.topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: labelContainerView.bottomAnchor),
rightLabel.leadingAnchor.constraint(equalTo: leftLabel.trailingAnchor),
rightLabel.topAnchor.constraint(equalTo: labelContainerView.topAnchor),
rightLabel.bottomAnchor.constraint(equalTo: labelContainerView.bottomAnchor),
rightLabel.trailingAnchor.constraint(equalTo: labelContainerView.trailingAnchor)
])
containerStackView.addArrangedSubview(labelContainerView)
containerStackView.addArrangedSubview(UIView())
}
}