I have a UILabel inside a UITableViewCell that I want to dynamically adjust its font and size based on content to always maintain the largest font that it can support without truncating. I am most of the way there using adjustsFontSizeToFitWidth, but the label has extra vertical space that it does not need.
In this screenshot running on the iPhone SE simulator, the label correctly scaled the font down from 55 to fit, but there is this extra space at the top of the label. I want that space to go away!
Here is my label code:
private lazy var label: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 55, weight: .ultraLight)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.25
label.backgroundColor = .purple
return label
}()
Here is my layout code:
self.addSubview(self.labelsContainerView)
self.labelsContainerView.snp.makeConstraints { (make) in
make.leading.equalTo(leftView.snp.trailing).offset(20)
make.trailing.equalTo(rightView.snp.leading).offset(-20)
make.top.bottom.equalToSuperview()
}
self.labelsContainerView.addSubview(self.middleLabel)
self.middleLabel.snp.makeConstraints { (make) in
make.leading.trailing.centerY.equalToSuperview()
}
self.labelsContainerView.addSubview(self.topLabel)
self.topLabel.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.bottom.equalTo(self.middleLabel.snp.top)
make.top.equalToSuperview().offset(8)
}
self.labelsContainerView.addSubview(self.bottomLabel)
self.bottomLabel.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.top.equalTo(self.middleLabel.snp.bottom)
make.bottom.equalToSuperview().offset(-8)
}
In short, I have a UIView pinned to the top and bottom of my UITableViewCell. Inside that view, I have 3 labels: top, middle, and bottom. The labels are pinned top-to-bottom.
I want middleLabel to always be the largest size it can be to satisfy layout and use the largest font that will fit inside that size without truncating. It seems almost like the intrinsic content size isn't being updated when the label's font changes. I have tried all kinds of calls to setNeedsLayout() and layoutIfNeeded() but they never helped.
It will take some extra work to get the label's Height to change, but if you only need the text vertically centered, add this to your label configuration:
label.baselineAdjustment = .alignCenters
Here's the difference... top uses default .alignBaselines bottom uses .alignCenters:
Related
I want to button 's top is align to the button's titleLabel's top , so I set the content vertical alignment to top in xib . this is works well in xib , but after build , the titleLabel seems still layout with center vertical alignment . What did I miss?
First, a comment: What you see in Storyboard / Interface Builder:
is not always exactly what UIKit renders in the Simulator
which is not always exactly what UIKit renders on a Device
This is why we test, test, test... on different simulators and devices.
So, what's going on here?
UIKit uses the frame of the button's .titleLabel to determine the button's intrinsic size.
For a default button, with no explicit width or height set, UIKit insets the title label by 6-pts on the Top and Bottom.
Here are 2 buttons - both with no Height constraint. Button 1 is the default Content Alignment of center/center, Button 2 is set to Left/Top. The button title label background is cyan, so we can easily see its frame.
Storyboard / IB:
Runtime:
Debug View Hierarchy (note the label frame vs the button frame):
So, with an 18-pt system font, the title label height is 21.0 ... add 6-pts top and bottom and the button frame height is 33-pts.
It doesn't matter whether you set the Control Alignment to Top / Center / Bottom or Fill ... UIKit still takes the label height and adds 6-pts Top and Bottom "padding."
What to do to get actual Top alignment? Let's look at a couple approaches.
Here are 6 buttons in a stack view:
Button 1 is at the default center/center, with no Height constraint.
Button 2 as we've seen, has Control Alignment Left / Top ... but has no effect on the vertical alignment.
Button 3 is also Left/Top, but let's give it an explicit Height (we'll use 80 to make things obvious). Looks better - but there is still 6-pts of "padding" added to the top of the title label.
Now, you may have seen this at the top of the Size Inspector panel:
This looks promising! Let's set the Title Insets Top to Zero!
Whoops -- it's already Zero?!?!?!?
Same thing with the Content Insets!
Turns out, if the the edge insets are at the default, the padding is added anyway.
We could try setting the Title Inset Top to -6 and, because labels center the text vertically, we'll also have to set the Bottom inset to 6. This works... but we may not want to rely on that value of 6 to be accurate in future iOS versions.
Button 4 - lets try Content Insets -> Bottom: 1 ... and it looks like we're on our way! The label is now top-aligned with the button frame!
So...
Button 5 - remove the Height: 80 constraint so we can use the default button height. D'oh! The button frame height is now title-label-height plus zero-top plus one-bottom...
Button 6 - finally, we'll use Content Insets -> Bottom: 1 with an explicit Height constraint of 33 (the default button height).
So... what if we don't want to set an explicit Height? Perhaps we're going to change the title label's font size later? Or we run into some other issue?
You could use a custom UIButton subclass.
Here's an example, marked as #IBDesignable so we can see its layout in Storyboard / IB:
#IBDesignable
class MyTopLeftAlignedButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
func commonInit() {
self.contentVerticalAlignment = .top
self.contentHorizontalAlignment = .left
// if we want to see the title label frame
//titleLabel?.backgroundColor = .cyan
}
override var bounds: CGRect {
didSet {
if let v = titleLabel {
var t = titleEdgeInsets
let h = (bounds.height - v.frame.height) * 0.5
t.top = -h
t.bottom = h
titleEdgeInsets = t
}
}
}
}
Edit - response to comment...
If your goal is to match the "Button 5" style - where the button height matches the title label height - best bet is probably...
Select the button, then in the Size Inspector panel use these settings:
The 0.1 Top and Bottom values will override the default 6-pt Top/Bottom "padding," reducing it to effectively Zero.
I am trying to offset the center starting point for a UILabel. The problem that I am facing is that I can't make the label text to grow from a point that is offset from center until it reaches one end of the label. Then, I want the text to shift one character to the left with each additional added character until it reaches the the other end. Then it would be acceptable for the text to truncate with an ellipses.
So far, my code looks like this but I don't know where to go from here.
private let amountLabel: UILabel = {
let label = UILabel()
label.textColor = .blue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
I suggest you put the UILabel into a container view, center horizontally with the offset you want, but set its priority lower than the compression resistance., e.g. 750, compression resistance 999. Then create a trailing constraint >= with priority 1000, and leading constraint >= priority 1000. In that way centering will be the weakest constraint and as the text grows it will shift to the left until it reaches the leading.
I cannot wrap and align at the same time an UILabel displayed in a UITableViewCell.
I want some UILabels (displayed below with a white background) to be right aligned
and word wrapped if the text is too long. To clarify the sreenshots below:
UILabel with a white background are the labels I am talking about
I am using two different types of cell (respectively with blue and orange background)
The UITableView has a something-like-pink background
The ViewController in which the UITableView is displayed has a light gray background
Either is the alignment correct but the text is not wrapped (Actually the text "Long.. " is long, please see the second screenshot)
Or the text is correctly wrapped but it is not right aligned:
My code is based on this tutorial: How to build a Table View with multiple cell types
Inside the code for the cell displayed with an orange background:
class AttributeCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel?
#IBOutlet weak var valueLabel: UILabel?
let orange = UIColor(red: 1, green: 165/255, blue: 0, alpha: 1)
var item: AttributeLabelLabel? {
didSet {
titleLabel?.backgroundColor = .white
titleLabel?.setLabel(contentText: (item?.attributeNameFromLocalizable)!, alignmentText: .right)
valueLabel?.backgroundColor = .green
valueLabel?.setLabel(contentText: (item?.attributeValue)!, alignmentText: .left)
self.backgroundColor = orange
}
}
static var nib:UINib {
return UINib(nibName: identifier, bundle: nil)
}
static var identifier: String {
return String(describing: self)
}
}
I added an extension to UILabel to set the alignment and text of the two labels displayed on cell, the way how the text should be wrapped is the same for all labels.
With the extension below the label is aligned but not wrapped (see first screenshot above).
extension UILabel{
func setLabel(contentText: String, alignmentText: NSTextAlignment){
self.text = contentText
self.textAlignment = alignmentText
self.numberOfLines = 0
self.lineBreakMode = .byWordWrapping // inefficient alone
}
}
If I want to have the text to be wrapped then I have to add a call to sizeToFit() but then short label (see label with the text "Short") is not right aligned (see second screenshot above).
extension UILabel{
func setLabel(contentText: String, alignmentText: NSTextAlignment){
self.text = contentText
self.textAlignment = alignmentText
self.numberOfLines = 0
self.lineBreakMode = .byWordWrapping
self.sizeToFit() // allow text to be effectivly wrapped
}
}
Why do I need to specify self.sizeToFit() on the documentation I have found only the use of lineBreakMode is mentionned to wrap a text ?
As I can not handle word wrapping and text alignement, I had the idea to compare the width of the UILabel with its text, and depending on the comparaison handling the alignment (for a text short enough) or the wrapping (if the text is too long). But I did not find how to get the UILabelĀ“s width.
Another idea would be to create a custom UILabel and set all constraint, compression and resistance in code. For now there are no constraints:
Has someone already dealt with such problems?
Is it possible to handle text wrapping and text alignement at the same time ?
Note:
On the second screenshot the UILabel with a wrapped text overlapped the cell boundaries. It is not the first problem and I can live with that for now but if someone has an hint about that...
I actually use the following code to deal with cell with different heights:
cell?.estimatedRowHeight = 200
cell?.rowHeight = UITableView.automaticDimension
You are missing a few constraints.
To get a multiline label to wrap, it must have its width limited (how else would it know the text is too long?).
To get auto layout to adjust the cell's height, you need constraints on the content of the cell to "push down" the bottom of the cell.
So...
Constrain your top-left label to Leading: 0, Top: 0, Width: 77 (I'm using 77 as the width, based on your images).
Constrain your top-right label to Leading: 8 (to top-left label's trailing), Top: 0, Trailing: 0
Constrain your bottom-left label to Leading: 0, Top: 8 (to top-left label's bottom), Width: 77 (or, width equal to top-left label)
Constrain your bottom-right label to Leading: 8 (to bottom-left label's trailing), Top: 8 (to top-right label's bottom, or Top: 0 to top of bottom-left label), Trailing: 0
then, add Bottom constraints of >= 0 to each of the bottom labels.
I'm guessing either bottom label may wrap to multiple lines, so set each one to Number of Lines: 0
The layout:
the result:
A UILabel can definitely be right-aligned and wrap on multiple lines. Here is an example:
Actually, the label content is misleading as it wraps on four lines! ;-)
This can be achieved through AutoLayout constraints and the right settings on the UILabel. This particular UILabel is constrained as follows:
Vertically centred
Leading edge to super view
Width
Here are the constraints, as shown in Interface Builder:
Finally, to have a UILabel line-wrap to multiple lines, its numberOfLines property needs to be set to 0, either through Interface Builder or code.
You can also right-align the text using the textAlignment property, setting it to .right, again through Interface Builder or code.
Given the view hierarchy:
UIStackView
--UILabel
--UISwitch
The label breaks too early, even if it can be fit to a single line.
Setting numberOfLines = 1 forces the label to be laid out correctly.
How to make UILabel perform line break only when needed?
Code:
private lazy var title: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
private lazy var toggle = UISwitch()
private lazy var stack = UIStackView(axis: .horizontal,
distribution: .equalSpacing,
alignment: .center,
views: [title,
toggle])
func setupConstraints() {
stack.snp.makeConstraints { (make) in
make.edges.equalTo(contentView.layoutMarginsGuide)
}
}
Result:
Setting numberOfLines = 1 gets me what I'd like to achieve, but the label looses its multi-line functionality:
How to force the desired behavior without losing support for multi-line labels?
When there is a lot of horizontal space, the label gets laid out correctly no matter of the numberOfLines property:
Set your UISwitch's content hugging and resistance priority to 1000.
And stack view distribution and alignment to fill.
Extra Note - If you want label and switch to be top aligned, then set alignment to top.
In your stack view you can give a constraint to your label and switch...
1) give your label leading, top , trailing and bottom constraint. Don't give Width constraint. In trailing constraint take second item Switch.
2) give your switch trailing, top, bottom and Fix width.
Hope it Will work.
Add label inside another stack view.
UIStackView
--UIStackView
--UILabel
--UISwitch
This is the UITableViewCell I have:
The three UILabels have trailing, top, bottom and leading constraints.
This is the hugging priority and compression resistance priority for the UILabel Name:
This is the hugging priority and compression resistance priority for the UILabel Location:
This is the hugging priority and compression resistance priority for the UILabel Type:
On the viewDidLoad from my UITableViewController I'm doing something like this:
self.tableView.estimatedRowHeight = 80
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.reloadData()
But when I run the app, the UITableViewCells don't self sizing:
What am I doing wrong or what do I need to do to make the UITableViewCells make them self sizing?
Edit
I set to 0 the numberOfLines for each UILabel, now the location UILabel doesn't appear
It is most likely that your Text turned out larger in runtime causing the total height in your cell View to turn out greater than the row height itself.
First equate all the hugging priority and then try changing the spacing distance between the Labels with the relation 'Greater than or Equal' and then set the constant to something small like zero. If you still get a constraint error, increase your tableView row height.
Alternative method:
Add all 3 UILabels into another UIView with spacing zero between them, do not set a height constraint for this UIView and just set it in the centerY of the cellView and spaced from the ImageView
Tip: Only change constraint priorities if your UI is what you want even after the constraints are broken, unless of course you know what you're doing
For ur case no need to vertical hugging priority,just give
label.numberOfLines = 0
that works fine.
The question says you want to resize the label, but based on your code it seems like you want to do it by resizing the tableview rows. If that's the case, the correct place to set row height in a tableView is:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 80
}
Where "return 80" means you want table rows to be of height 80. Please let me know if I didn't understand your question right.
The label doesn't work because the storyboard is not very useful.
Set the var:
var imageView: UIImageView ={
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var textView: UITextView ={
let text = UITextView()
text.translatesAutoresizingMaskIntoConstraints = false
return text
}()
And you continue with other label.
You have to set constraints like this example:
self.addSubview(imageView)
self.addSubview(textView)
imageView.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor, constant: -10).active=true
imageView.leftAnchor.constraintEqualToAnchor(self.leftAnchor, constant:-10).active=true
imageView.widthAnchor.constraintEqualToConstant(70).active=true
imageView.heightAnchor.constraintEqualToConstant(70).active=true
label.topAnchor.constraintEqualToAnchor(self.topAnchor, constant: -5).active=true
label.leftAnchor.constraintEqualToAnchor(imageView.rightAnchor, constant: 3).active=true
label.widthAnchor.constraintEqualToAnchor(self.widthAnchor, constant -70).active=true
label.heightAnchor.constraintEqualToConstant(30).active=true
label2.topAnchor.constraintEqualToAnchor(label.bottomAnchor, constant: 5).active=true
label2.leftAnchor.constraintEqualToAnchor(imageView.rightAnchor, constant: 3).active=true
label2.widthAnchor.constraintEqualToAnchor(self.widthAnchor, constant -70).active=true
label2.heightAnchor.constraintEqualToConstant(30).active=true
And with this way also for the third label.