UILabel can not be wrapped and aligned at the same time - ios

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.

Related

UIButton not render exactly as XIB's contentVerticalAlignment value

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.

UILabel not resizing after scaling font using minimumScaleFactor

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:

UILabel breaks early inside UIStackView

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

Swift UILabel with horizontal lines on both sides

I am trying to create a UILabel that has 2 horizontal lines on the left and right side like this:
Does anyone know the best approach for doing this in Swift? The content text in the center will change so I want to make sure it can adapt. I'd really like to create some kind of reusable UIView class but I'm not sure where to start?
Thank you!
You can take two UIview of height 1 or 2 pixels of both side of the label. so it's look likes line!!
And you should set background color to black of that view!
Hope this will help :)
Take one UIView with height of 2. Set leading & Trailing according to Super View.
Now take one UILabel with background color white and put Vertically Center to line view.
Make both Center same.
Your work done.
For more help please refer below image.
You can use an extension on UILabel
public extension UILabel {
func drawLineOnBothSides(labelWidth: CGFloat, color: UIColor) {
let fontAttributes = [NSFontAttributeName: self.font]
let size = self.text?.size(attributes: fontAttributes)
let widthOfString = size!.width
let width = CGFloat(1)
let leftLine = UIView(frame: CGRect(x: 0, y: self.frame.height/2 - width/2, width: labelWidth/2 - widthOfString/2 - 10, height: width))
leftLine.backgroundColor = color
self.addSubview(leftLine)
let rightLine = UIView(frame: CGRect(x: labelWidth/2 + widthOfString/2 + 10, y: self.frame.height/2 - width/2, width: labelWidth/2 - widthOfString/2 - 10, height: width))
rightLine.backgroundColor = color
self.addSubview(rightLine)
}
}
This will add a horizontal line of width of 1.0 on the both side of your label. If you don't add text for your label, it will show two horizontal lines through center with some spaces in between them.
As others have mentioned, you can achieve this using three views:
Add a View to your scene to use as a container. I called this view "Lined Label Holder."
To that container, add two Views, one to produce the line on either side of the label.
Add the label in between the two views, and give it some text. Due to the "height = Test.height" constraint on the Lined Label Holder, The intrinsic height of this label is used to calculate the container's height.
The label is allowed to grow with added text and the lines will always start 5px away from the edges of the text and extended to the edges of the container, whose width can be set independently.
This image shows the required constraints:
Use one UIView with black background and height of 1px, set label background to white, align its text to center and align UILabel to center of UIView (there is no need for 2 views since label white background will cover UIView).
Not necessary 2 UIView's.
Take 1 UIView and give background black color.Add the constraints necessary with: height=2.
place 1 label on the center and give required constraints

Dynamic height UITableViewCell

I have cells that consist of a title, date and a number of hashtags.
Here's the storyboard's screenshot:
Custom cell in storyboard
I've set the following in my ViewDidLoad:
tableView.rowHeight = UITableViewAutomaticDimension
But haven't given any estimatedHeight for the tableView which I'll explain why.
Here's my customCell:
#IBOutlet weak var title: UILabel!
#IBOutlet weak var date: UILabel!
#IBOutlet weak var hashtagsView: UIView!
var item: Item? {
didSet {
configureCell()
}
}
func configureCell() {
if item = item {
title.text = item!.title
date.text = item!.date
// The part where I calculate the sizes of hashtags to fit them in hashtagsView
let totalWidth = CGRectGetWidth(hashtagsView.frame)
print("TotalWidth: \(totalWidth)")
.
.
.
.
print("Content: \(self.contentView.frame)")
print("HashtagsView: \(self.hashtagsView.frame)")
}
}
Here's the results:
With tableView.estimatedHeight = 150
TotalWidth: 240.0
Content: (0.0, 0.0, 240.0, 119.666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Without estimatedHeight
TotalWidth: 240.0
Content: (0.0, 0.0, 414.0, 43.6666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Recap
When there is an estimatedHeight, cell's contentView doesn't print a correct width, but displays the cell's contents well like nothing's wrong (except for the hashtagsView).
When there's not an estimatedHeight, cell's contentView does actually print the correct width, which lets me calculate the hashtags frames, but the cells display with their default 44 height.
Detailed Info
Since I don't know how many hashtags there is, I'm trying to use the blank UIView to add the UIButtons programmatically. And for calculation purposes of the hashtags' buttons, I need to have the "width" of the cell's contentView, or the hashtagsView's, but when I set tableView.estimatedHeight, cell's width will be some arbitrary number (e.g. 240 in 6s Plus Simulator). And I just can't get the hashtagsView's width, even though I have no auto-layout issues.
And when I don't give tableView.estimatedHeight an estimation, I get the following:
Custom cells without estimatedHeight
Updated - An update asked by #EarlGray in the comments
The hashtags are actually UIButtons I add them to the hashtagsView dynamically. I need to stretch the hashtagsView's height so it'll fit more than one line of hashtags.
I think I'll either need to subclass UIView and override layoutSubviews() to achieve fit vertical layout or add constraints to each subview (UIButtons) programmatically.
Doing what #VinodVishwanath said, setting estimatedRowHeight combined with explicit heights and vertical space constraints gives me this:
Which unables me to get the width of the hashtagsView. During the calculations of the UIBUttons' frames, I need the cell.contentView's width, but somehow, setting the estimatedRowHeight gives me the following coordinates for the cell.contentView.frame
Content: (0.0, 0.0, 240.0, 119.666666666667)
Which is incorrect, because it has to give 414, and that's why my hashtags start from half of the screen.
Commenting out tableView.estimatedRowHeight gives me the correct coordinates:
Content: (0.0, 0.0, 414.0, 43.6666666666667)
But messes my tableView like so:
Update #2 - Here's my constraints for the cell.contentView
ContentView's constraints
Update #3 - A breakpoint on my configureCell method
HashtagsView's superview returns nil!!
I don't get it, my UIView IBOutlet is connected, I double checked.
All of the contentView.subviews have incorrect frames. So does the superview-less hashtagsView.
But when I remove estimatedRowHeight, it suddenly considers hashtagsView as a subview of cell's contentView. Except for contentView, it's subviews frames' still return incorrect and negative values.
When you don't provide the estimated row-height, the cell height defaults to 44, and that's why you're getting a content height of 43.667.
You need to provide the estimated row height, which will be the height when there are no tags in the field provided. Then all that's needed to set the correct height dynamically is the right set of autolayout constraints to provide the contentView's intrinsic size.
This will happen when you set a vertical space constraint between all the subviews of contentView, including a topSpaceToSuperview and bottomSpaceToSuperview to enable the AutoLayout engine to calculate the intrinsic content size.
Edit
I have analysed your constraints from the screenshot, and here are the relevant constraints you have added to calculate the cell height:
Title.top = topMargin + 8
bottomMargin = Share Button.bottom
Link.centerY = Share Button.centerY
Hashtags View.top = Title.bottom
Date.top = Hashtags View.bottom + 15
Now here's a visual representation of these constraints:
TopMargin
|
Title
|
Hashtags View
|
Date
!!! broken link !!!
Link — Share Button
|
BottomMargin
The broken link prevents the Layout Engine from calculating the cell height. You need to fix the broken link in the vertical layout, for instance, by setting Date.centerY = shareButton.centerY, or by setting Date.top = HashtagsView.bottom.

Resources