UIStackView spacing not working for nested stack views with baseline alignment - swift

I'm attempting to include a series of UITextFields, with corresponding UILabels, in a vertical UIStackView. I'm nesting each label/field pair in a horizontal UIStackView, and the width of the labels is hard-coded to the widest intrinsic width of all the labels, so that they line up nicely.
It very nearly works, but the horizontal stack views are not spaced evenly as expected (spacing = 5) and are bunched up and overlapping.
HOWVER, if I delete the line hFieldStack.alignment = .lastBaseline, then it all works perfectly.
I'm OK with deleting that line, as it looks fine. But I'm just curious as to why spacing in the outside stack view doesn't work when using lastBaseline alignment within the nested stack view.
stackView.axis = .vertical
stackView.spacing = 5
stackView.layoutMargins = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)
stackView.isLayoutMarginsRelativeArrangement = true
if let titleLabel = titleLabel {
stackView.addArrangedSubview(titleLabel)
}
if let messageLabel = messageLabel {
stackView.addArrangedSubview(messageLabel)
}
if textFields.count == fieldLabels.count {
for i in 0..<textFields.count {
fieldLabels[i].widthAnchor.constraint(equalToConstant: 80).isActive = true
let hFieldStack = UIStackView()
hFieldStack.translatesAutoresizingMaskIntoConstraints = false
hFieldStack.axis = .horizontal
hFieldStack.alignment = .lastBaseline
hFieldStack.spacing = 5
hFieldStack.layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
hFieldStack.isLayoutMarginsRelativeArrangement = true
hFieldStack.addArrangedSubview(fieldLabels[i])
hFieldStack.addArrangedSubview(textFields[i])
stackView.addArrangedSubview(hFieldStack)
}
} else {
logger.error("Field count doesn't match label count")
}

Curious...
It appears that, when setting the horizontal stack view's Alignment to .lastBaseline (also happens with .firstBaseline), Auto-layout is using the text field's _UITextFieldCanvasView to calculate the layout.
So, with a rounded-rect border, the actual frame of the text field is taller than the _UITextFieldCanvasView.
We can confirm this by inspecting the view hierarchy:
The labels are nicely baseline aligned with the text rendered in the _UITextFieldCanvasView... but the frames are obviously extending outside the stack view's framing.
While the docs do list .lastBaseline as. valid Alignment constant, it's somewhat telling that, in Storyboard / Interface Builder, the only Alignment options are Fill, Leading, Center and Trailing.
I've come across several actual Bugs related to StackViews --- so I think I would (personally) add this one to the list.

Related

Changing title in uibutton in stackview inside scrollview using swift

Could you give me some help please about why paddings for button title isn't working, please?
I have the code below from where I update the button's titles n times. Buttons are inside a horizontal scrollview. Inside scrollview I have a stackview. From outside the scroll, I update the titles and paddings are out from my control.
The problem is the following: if I give a huge title for the button and then I change for a short title, the paddings aren't updated as I determined in the code below.
private func setupActionButtons(index: Int = 0, title: String = "") {
allButtons[index].layer.cornerRadius = allButtons[0].bounds.height/2
allButtons[index].layer.borderWidth = 1
allButtons[index].layer.borderColor = UIColor.borderShortcutButton.cgColor
allButtons[index].setTitleColor(.psGreyValueLabelFilled, for: .normal)
if title != "" {
allButtons[index].setTitle(title, for: .normal)
}
allButtons[index].titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
allButtons[index].contentEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 15)
allButtons[index].imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
allButtons[index].imageView?.contentMode = .scaleAspectFit
allButtons[index].setImage(uiImage, for: .normal)
allButtons[index].semanticContentAttribute = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .forceLeftToRight : .forceRightToLeft
allButtons[index].titleLabel?.font = UIFont.systemFont(ofSize: buttonFontSize, weight: UIFont.Weight.semibold)
allButtons[index].tag = index
allButtons[index].addTarget(self, action: #selector(self.handleSelectOption(sender:)), for: .touchUpInside)
}
explaining better: when I add title with similar width for two buttons inside the stackview and after that I change the title for one of the buttons to a less wide, the other button gets with a bigger width with a bigger padding and the other I changed arranges correctly.
The problem is setting the stack view's Distribution to Fill Proportionally
With the stack view set to Fill the problem goes away.
As a side note: forget you ever heard about Fill Proportionally ... it is the most misunderstood and misused setting for stack views, and unless you know exactly how it works and exactly why you want to use it, you won't get your desired layout.

Swift 5: centerXAnchor won't center my frame in the view

I'm stuck at the Consolidation IV challenge from hackingwithswift.com.
Right now I'm trying to create a hangman game. I thought to place placeholder labels based on the length of the answer word. These placeholder labels would be placed inside a frame, which then would be placed in the center of the main view.
Unfortunately, the leading edge of the frame is placed centered. In my opinion, this is not a problem of constraints, but rather a problem of me creating the frame wrong.
My current code is
import UIKit
class ViewController: UIViewController {
var answer: String!
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - declare all the labels here
let letterView = UIView()
letterView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(letterView)
// MARK: - set constraints to all labels, buttons etc.
NSLayoutConstraint.activate([
letterView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
letterView.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
// MARK: - populate the letterView
// set the size of the placeholder
answer = "Atmosphäre"
let height = 60
let width = 25
// var width: Int
for placeholder in 0..<answer.count {
// create new label and give it a big font size
let placeholderLabel = UILabel()
placeholderLabel.text = "_"
placeholderLabel.font = UIFont.systemFont(ofSize: 36)
// calculate frame of this label
let frame = CGRect(x: placeholder * width, y: height, width: width, height: height)
placeholderLabel.frame = frame
// add label to the label view
letterView.addSubview(placeholderLabel)
}
}
}
The simulator screen looks just like this:
I already searched for answers on stackoverflow, but wasn't successful. I think I don't know what I'm exactly looking for.
The main problem, is that the letterView has no size, because no width or height constraints are applied to it.
To fix your code make the letterView big enough to contain the labels you've added as subviews by adding height and width constraints after the for loop:
for placeholder in 0..<answer.count {
...
}
NSLayoutConstraint.activate([
letterView.widthAnchor.constraint(equalToConstant: CGFloat(width * answer.count)),
letterView.heightAnchor.constraint(equalToConstant: CGFloat(height))
])
I'm not sure if you've covered this in your course yet, but a better way to go about this (which would take much less code), is to use a UIStackView as your letterView instead.
An extra thing to consider:
If you give the letterView a background color, you'll see that the labels are actually aligned outside of its bounds:
That's because you're setting each label's y position to be height, when it should probably be zero:
let frame = CGRect(x: placeholder * width, y: 0, width: width, height: height)
Correcting this places the labels within the bounds of the letterView:

Creating UILabel programmatically with constraints?

What I want to do is create a new UILabel programmatically every time a certain action occurs in my code. I know the x, y, and height that I want to give the label, but I don't want to give it a set width. I want to constrain the sides so that the UILabel width is equal to the width of the screen, and so that the label width will change if the orientation is flipped.
I have considered using:
CGRect(x:, y:, width:, height:)
However, I would have to give it a set width if I use this, so I don't think it will work.
I also tried using:
CGPoint(x:, y:)
Then setting leading, trailing and height anchors, however, this doesn't seem to work either as even though it does compile, I get an error when I try creating a new UILabel.
I'm kind of new to programming in Swift so I'm not sure if there is an obvious fix to this.
Like you said, we already have x, y and height available for the label, i.e.
let x: CGFloat = 0
let y: CGFloat = 0
let height: CGFloat = 50
Let's create a label using the above details. Also set the width of the label as UIScreen.main.bounds.width as per your requirement.
let label = UILabel(frame: CGRect(x: x, y: y, width: UIScreen.main.bounds.width, height: height))
label.text = "This is a sample text"
Don't forget to set label's translatesAutoresizingMaskIntoConstraints as false and add it to whatever subview you want.
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
Add the relevant constraints of label with its superview - top, bottom, leading, height. You can definitely add bottom constraint if required. That totally depends upon your UI.
I'm adding the label to the top of the viewController's view.
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: height),
view.topAnchor.constraint(equalTo: label.topAnchor),
view.leadingAnchor.constraint(equalTo: label.leadingAnchor),
view.trailingAnchor.constraint(equalTo: label.trailingAnchor)
])
You can use following code to create UILabel programmatically.
private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = "Hello World"
return label
}()
Then inside your viewDidLoad()
addSubview(label)
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: label.topAnchor),
bottomAnchor.constraint(equalTo: label.bottomAnchor),
leadingAnchor.constraint(equalTo: label.leadingAnchor),
trailingAnchor.constraint(equalTo: label.trailingAnchor)
])

How to use frame to set x position in loop on Swift

I have an array that count is between 1 to 3 varies.
I want to frame them in the middle of the screen with a certain distance
Also the width of the labels is fixed
let array = ["some1", "some2", "some3"]
func setLabel(){
var i = -1
for text in array{
i += 1
let label = UILabel()
label.fram = CGRect(x: screenWidth/2 - (CGFloat(i)*50) + 25, y: 100, width: 50 , height: 20)
label.text = text
addSubview(label)
}
}
You can use stackview as suggested by #matt. If you don't want to use stackview you can calculate the x position by array count and label index like this
let array = ["some1", "some2", "some3"]
func setLabel(){
var i:CGFloat = 0
for text in array{
i += 1
let label = UILabel()
label.backgroundColor = .red
label.frame = CGRect(x: (view.bounds.width/CGFloat(array.count+1))*i-25, y: 100, width: 50 , height: 20)
label.text = text
view.addSubview(label)
}
}
The really easy way to do this is to put the labels (one, two, or three of them) into a centered UIStackView. You would still need to know the desired fixed width of a label and apply a width constraint to each label, but everything else will just take care of itself.
Just as a demonstration, I used an arbitrary width of 100, with the text for all three labels being just "UILabel" as in your image. Here's what it looks like with one label:
Here's what it looks like with three labels:
Here's the code I used (sv is the stack view, which has already been configured in the storyboard; n is how many labels we want):
for _ in 1...n {
let lab = UILabel()
lab.text = "UILabel"
lab.textAlignment = .center
lab.translatesAutoresizingMaskIntoConstraints = false
lab.widthAnchor.constraint(equalToConstant: 100).isActive = true
sv.addArrangedSubview(lab)
}
So all you really need to do is get the right text from your array as we loop, and you're done. The point is that you don't need to think about frames at all! The UIStackView imposes the correct the frames for us.
NOTE Although I said above that the stack view was "configured in the storyboard", that has nothing to do with the answer. You can create and configure the stack view in code just as well, without changing anything about my answer. The point is that the stack view already knows how to receive an arbitrary number of views, space them out evenly, and center them as a whole. That is what it is for. So if, as your question implies, you can't manage the arithmetic to assign the views a frame yourself, why not let the stack view do it for you?

Programmatically update UITextView height

I am trying to update the height of a UITextView based on the content. I have seen this solution but cannot get it to work with my current code (still learning swift)
I define the UITextView as such:
let eventDetailInfoTextBox : UITextView = {
let textbox = UITextView()
textbox.backgroundColor = UIColor.clear
textbox.layer.borderWidth = 1
textbox.layer.borderColor = ColorPallet.AppTertiaryColor.cgColor
textbox.layer.cornerRadius = 10
textbox.setNeedsDisplay()
let contentSize = textbox.sizeThatFits(textbox.bounds.size)
textbox.isEditable = false
return textbox
}()
The subview is then added in setupViews() along with defining its position in the view using a call to setupEventDetailInfoTextBox()
fileprivate func setupEventDetailInfoTextbox() {
print("Event Detail Info Text Box Height: \(eventDetailInfoTextBox.contentSize.height)")
var frame = eventDetailInfoTextBox.frame
frame.size.height = eventDetailInfoTextBox.contentSize.height
eventDetailInfoTextBox.anchor(eventDetailMapView.bottomAnchor, left: self.leftAnchor, bottom: nil, right: self.rightAnchor, topConstant: 8, leftConstant: 10, bottomConstant: 0, rightConstant: 10, widthConstant: 0, heightConstant: frame.size.height)
}
The call to .anchor is based on the Lets Build That App frameworks found via this link and basically wraps up the local functions from Xcode. I know this works and have reused the same function repeatedly throughout my app.
The output from the print statement is -8 and is represented by a height suitable to show 1 line of text (sometimes).
Can anyone see why my text box refuses to get any bigger if I have more than 1 line of text?
I'm using IOS 10, Xcode 8 and writing in swift 3.
Follow these step
Create height constraint outlet of TextView.
Calculate content height of Textview.
Update TextView height constraint constant value.
Call layoutIfNeeded() function.
I hope it will work.
If you want something static, down and dirty, you can streamline the process (not set and reset properties) as long as you place everything in the right order. If you need to handle device rotations or other runtime changes, that would require something slightly less down and dirty, but not by much--I'm not sure which you need so I opted for the simpler.
// instance property so that other constraints can refer to it
let descriptionTextView = UITextView()
// configure text view
descriptionTextView.text = descriptionModel
descriptionTextView.font = // UIFont
descriptionTextView.textColor = // UIColor
descriptionTextView.isEditable = false
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(descriptionTextView)
// set constraints
descriptionTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true
descriptionTextView.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 32).isActive = true
descriptionTextView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -32).isActive = true
descriptionTextView.sizeToFit()
descriptionTextView.layoutIfNeeded()
descriptionTextView.heightAnchor.constraint(equalToConstant: descriptionTextView.contentSize.height).isActive = true