UILabel's height is to big for it's width - ios

As you can see right here, this is a UILabel with random text:
The height of that UILabel's text is too big. I just want the height to be adapted from what it is needed to be, for every different content and size of the width. Adding this extensions:
extension UILabel{
func requiredHeight() -> CGFloat{
let label:UILabel = UILabel(frame: CGRect(0, 0, self.frame.width, CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = self.font
label.text = self.text
label.sizeToFit()
return label.frame.height
}
}
extension CGRect{
init(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat) {
self.init(x:x,y:y,width:width,height:height)
}
}
are not working aswell... Is this possible to do in Storyboard? I tried setting an aspect ratio to it, but when programmatically making the label larger this fails. Is there an easy way to set the height of a UILabel to match it's content, and maybe even in storyboard? Because of the height is not corresponding with what it is needed to be, my whole layout is screwing up. The UILabel needs to be for example 10 points from the top layout. Because the height is to big, the UILabel is setting itself far more below than needed when running it on a different device than the settee layout in storyboard.

I think you can make it setting top and bottom constraints of your UILabel in Storyboard
In this example independent of the font size of "LABEL BIG" there will always be a separation from top layout of 10 points and the "LABEL SMALL" will always be vertically separated by 8 points from "LABEL BIG"

Related

Size UITextView to fit multiline NSAttributedString

I have a UITextView containing an NSAttributedString. I want to size the text view so that, given a fixed width, it shows the entire string without scrolling.
NSAttributedString has a method which allows to compute its bounding rect for a given size
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
But unfortunately it seems not working, since it always returns the height of a single line.
After several attempts, I figured out that the NSAttributedString I was setting had byTruncatingTail as lineBreakMode value for NSParagraphStyle (which is the default value we use in our application).
To achieve the desired behaviour I have to change it to byWordWrapping or byCharWrapping.
let paragraphStyle = NSMutableParagraphStyle()
// When setting "byTruncatingTail" it returns a single line height
// paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineBreakMode = .byWordWrapping
let stringAttributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Avenir-Book", size: 16.0)!,
.paragraphStyle: paragraphStyle]
let attributedString = NSAttributedString(string: string,
attributes: stringAttributes)
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
computedSize.height
Note that when setting the attributed string with byTruncatingTail value on a UILabel (where numberOfLines value is 0), the string is "automatically" sized to be multiline, which doesn't happen when computing the boundingRect.
There are other factors to keep in mind when computing NSAttributedString height for use inside a UITextView (each one of these can cause the string not to be entirely contained in the text view):
1. Recompute height when bounds change
Since height is based on bounds, it should be recomputed when bounds change. This can be achieved using KVO on bounds keypath, invalidating the layout when this change.
observe(\.bounds) { (_, _) in
invalidateIntrinsicContentSize()
layoutIfNeeded()
}
In my case I'm invalidating intrinsicContentSize of my custom UITextView since is the way I size it based on the computed string height.
2. Use NSTextContainer width
Use textContainer.width (instead of bounds.width) as the fixed width to use for boundingRect method call, since it keeps any textContainerInset value into account (although left and right default values are 0)
3. Add vertical textContainerInsets values to string height
After computing NSAttributedString height we should add textContainerInsets.top and textContainerInsets.bottom to compute the correct UITextField height (their default values is 8.0...)
override var intrinsicContentSize: CGSize {
let computedHeight = attributedText.boundingHeight(forFixedWidth: textContainer.size.width)
return CGSize(width: bounds.width,
height: computedHeight + textContainerInset.top + textContainerInset.bottom)
}
4. Remove lineFragmentPadding
Set 0 as value of lineFragmentPadding or, if you want to have it, remember to remove its value from the "fixed width" before computing NSAttributedString height
textView.textContainer.lineFragmentPadding = 0
5. Apply ceil to computed height
The height value returned by boundingRect can be fractional, if we use as it is it can potentially cause the last line not to be shown. Pass it to the ceil function to obtain the upper integer value, to avoid down rounding.
A possible way to do it, is to subclass UITextView to inform you whenever its contentSize did change (~ the size of the text).
class MyExpandableTextView: UITextView {
var onDidChangeContentSize: ((CGSize) -> Void)?
override var contentSize: CGSize {
didSet {
onDidChangeContentSize?(contentSize)
}
}
}
On the "Parent View":
#IBOulet var expandableTextView: MyExpandableTextView! //Do not forget to set the class in the Xib/Storyboard
// or
var expandableTextView = MyExpandableTextView()
And applying the effect:
expandableTextView. onDidChangeContentSize = { [weak self] newSize in
// if you have a NSLayoutConstraint on the height:
// self?.myExpandableTextViewHeightConstraint.constant = newSize.height
// else if you play with "frames"
// self?.expandableTextView.frame.height = newSize.height
}

UITextView dynamic sizing is making the textView too tall

I am using this link to resize a textView dynamically. However, I implement it into a function and it return a height of 238. Using the heirarchy viewer I can see my textView is indeed 238, but there is a ton of empty space as 238 is way taller than needed. Can anyone figure out why textView.sizeThatFits: would give me this kind of error? Here is my code.
func heightOfTextFieldForMessage(message : String) -> CGFloat {
let horizontalMargin = CGFloat(194)
let textView = UITextView()
textView.text = message
textView.font = UIFont.systemFontOfSize(14)
//This width is same as in heirachy viewer, horizontalMargin is a constant distance the textView is from edges of the screen.
let fixedWidth = self.view.frame.width - horizontalMargin
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.max))
let height = newSize.height
print(height)
return height
}
Is it possibly because my font is smaller than the default font? I tried changing the textView's font size before resizing but it seems like like the height does change based on what I set the font to.
The Text View bottom constraint is tied to the top of the icon so as you can see the textView is way taller than needed.

UILabel subclass - text cut off in bottom despite label being correct height

I have a problem with UILabel subclass cutting off text in the bottom. Label is of proper height to fit the text, there is some space left in the bottom, but the text is still being cut off.
The red stripes are border added to label's layer.
I subclass the label to add edge insets.
override func sizeThatFits(size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
return size
}
override func drawTextInRect(rect: CGRect) {
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
However, in this particular case the insets are zero.
Turns out the problem was with
self.lineBreakMode = .ByClipping
changing it to
self.lineBreakMode = .ByCharWrapping
Solved the problem
I was facing the same issue with Helvetica Neue Condensed Bold font. Changing label's Baseline property from Align Baselines to Align Centers did the trick for me. You can change this easily in storyboard by selecting your label.
My problem was that the label's (vertical) content compression resistance priority was not high enough; setting it to required (1000) fixed it.
It looks like the other non-OP answers may be some sort of workaround for this same underlying issue.
TL'DR
Probably the property you are looking for is UILabel's baselineAdjustment.
It is needed because of an old UILabel's known bug. Try it:
label.baselineAdjustment = .none
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Explanation
It's a bug
There is some discussions like this one about a bug on UILabel's text bounding box. What we observe here in our case is some version of this bug. It looks like the bounding box grows in height when we shrink the text through AutoShrink .minimumFontScale or .minimumFontSize.
As a consequence, the bounding box grows bigger than the line height and the visible portion of UILabel's height. That said, with baselineAdjustment property set to it's default state, .alignBaselines, text aligns to the cropped bottom and we could observe line clipping.
Understanding this behaviour is crucial to explain why set .alignCenters solve some problems but not others. Just center text on the bigger bounding box could still clip it.
Solution
So the best approach is to set
label.baselineAdjustment = .none
The documentation for the .none case said:
Adjust text relative to the top-left corner of the bounding box. This
is the default adjustment.
Since bonding box origin matches the label's frame, it should fix any problem for a one-lined label with AutoShrink enabled.
Also it could be changed through interface builder. This property could be found under UILabel's Attributes inspector with the name "Baseline".
Documentation
You could read more here about UILabel's baselineAdjustmenton official documentation.
Happened for me when providing topAnchor and centerYAnchor for label at the same time.
Leaving just one anchor fixed the problem.
Other answers didn't help me, but what did was constraining the height of the label to whatever height it needed, like so:
let unconstrainedSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
label.heightAnchor.constraint(equalToConstant: label.sizeThatFits(unconstrainedSize).height).isActive = true
Also, sizeThatFits(_:) will return a 0 by 0 size if your label's text field is nil or equal to ""
I ran into this too, but wanted to avoid adding a height constraint. I'd already created a UILabel subclass that allowed me to add content insets (but for the purpose of setting tableHeaderView straight to a label without having to contain it in another view). Using the class I could set the bottom inset to solve the issue with the font clipping.
import UIKit
#IBDesignable class InsetLabel: UILabel {
#IBInspectable var topInset: CGFloat = 16
#IBInspectable var bottomInset: CGFloat = 16
#IBInspectable var leftInset: CGFloat = 16
#IBInspectable var rightInset: CGFloat = 16
var insets: UIEdgeInsets {
get {
return UIEdgeInsets(
top: topInset,
left: leftInset,
bottom: bottomInset,
right: rightInset
)
}
}
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
return addInsetsTo(size: super.intrinsicContentSize)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return addInsetsTo(size: super.sizeThatFits(size))
}
func addInsetsTo(size: CGSize) -> CGSize {
return CGSize(
width: size.width + leftInset + rightInset,
height: size.height + topInset + bottomInset
)
}
}
This could be simplified just for the font clipping to:
import UIKit
class FontFittingLabel: UILabel {
var inset: CGFloat = 16 // Adjust this
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: UIEdgeInsets(
top: 0,
left: 0,
bottom: inset,
right: 0
)))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(
width: size.width,
height: size.height + inset
)
}
}
I had a vertical UIStackView with a UILabel at the bottom. This UILabel was cutting off the letters that go below the baseline (q, g, y, etc), but only when nested inside a horizontal UIStackView. The fix was to add the .lastBaseline alignment modifier to the outer stack view.
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
aVerticalStackWithUILabelAtBottom, // <-- bottom UILabel was cutoff
UIView(),
someOtherView
])
stackView.axis = .horizontal
stackView.spacing = Spacing.one
stackView.alignment = .lastBaseline // <-- BOOM fixed it
stackView.isUserInteractionEnabled = true
return stackView
}()

Is it possible to have a multiline textLabel in a UITableViewHeaderFooterView (without using a custom UILabel)?

I'm trying to use the built-in textLabel in a UITableViewHeaderFooterView in order display titles in the section headers of a UITableView.
These titles have an unknown amount of content and so need to cover multiple lines.
If this was a table cell then myCell.numberOfLines = 0 would work (along with estimatedHeightForRowAtIndexPath returning UITableViewAutomaticDimension). But I can't get anything similar to work with table headers.
I've tried setting textLabel.numberOfLines = 0 in viewForHeaderInSection and/or in willDisplayHeaderView. I've also tried setting it in a custom subclass I've created that the headers are using (set up with let sectionHeader = tableView.dequeueReusableHeaderFooterViewWithIdentifier("myIdentifier") as MyTableSectionHeaderSubclass). In that subclass I've tried setting textLabel.numberOfLines = 0 in the init function, as well as in layoutSubviews()
I've already set the correct height of each header by calculating the amount of space the text string will take up (using CGSizeMake in heightForHeaderInSection, can provide more info about this if it's of any help). So, there's enough vertical space for the labels to expand - they're just stuck on one line, with their text cut off and ending with an ellipsis.
I'm trying this approach in order to avoid using a custom UILabel to display the title. While I can apply multiline that way, this brings other problems such as label position/frame being lost when table rows are added or deleted.
Does anyone know if multi-line text is even possible with a UITableViewHeaderFooterView's built-in textLabel? Or is a custom UILabel my only option?
Many thanks!
I do think using a custom UILabel is a better approach as you can take control on all attributes.
First of all, a handy function to calculate the UILabel height. (Below is my version for my specific project). Notice that I set the NSMutableParagraphStyle which I think is the best way to handle line break, line spacing etc.
internal func heightForLabel(attributedString:NSMutableAttributedString, font:UIFont, width:CGFloat, lineSpacing: CGFloat) -> CGFloat{
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
let label:UILabel = UILabel(frame: CGRect(x:0, y:0, width:width, height:CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.font = font
label.textAlignment = .left
attributedString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
label.attributedText = attributedString
label.sizeToFit()
return label.frame.height
}
Then in your view controller, pre-calculate the section header heights
fileprivate let sectionHeaders = ["Header1", "Loooooooooooooooooooooong Header which occupies two lines"]
fileprivate var sectionHeaderHeights : [CGFloat] = []
override func viewDidLoad() {
super.viewDidLoad()
//Calculate the label height for each section headers, then plus top and down paddings if there is any. Store the value to `sectionHeaderHeights`
}
UITableViewDelegate methods
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeaderHeights[section]
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sectionHeader = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: sectionHeaderHeights[section]-paddings))
sectionHeader.backgroundColor = .clear
let sectionTitleLabel = UILabel()
sectionTitleLabel.text = sectionTitles[section]
sectionTitleLabel.font = UIFont(name: "GothamPro", size: 18)
sectionTitleLabel.textColor = .black
sectionTitleLabel.backgroundColor = .clear
sectionTitleLabel.frame = CGRect(x: padding, y: sectionHeader.frame.midY, width: sectionTitleLabel.frame.width, height: sectionTitleLabel.frame.height)
sectionHeader.addSubview(sectionTitleLabel)
return sectionHeader
}
In the Interface Builder drug the Label above the prototype cell. In the Attribute Inspector set number of lines = 0, Line Break to Word Wrap and enter your text.
Use this one:
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
Keep in mind that autosizing works only with:
lineBreakMode="tailTruncation"
numberOfLines="0"
One you do that, make sure that inside your cell the contentView constraints are set to top, left, bottom, right related to your textfield.
This way iOS will size the cell to textfield size.

How do I set adaptive multiline UILabel text?

I have a UILabel named titleLabel in my storyboard nib set to its default height. I want it to programatically expand in height to fit it's content. Here is what I have tried so far:
// just setting content
titleLabel.text = "You don't always know what you are getting with mass-market cloud computing services. But with SimpliCompute, the picture is clear. SimpliCompute gives you powerful virtual servers you can deploy using just your web browser. That’s enterprise grade technology you can deploy and control on-the-fly."
titleLabel.numberOfLines = 0
titleLabel.preferredMaxLayoutWidth = 700
titleLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
titleLabel.sizeToFit()
None of this works for me in any combination! I always only see one line of text in my UILabel. What am I doing wrong?
I absolutely need the text content to be variable.
I kind of got things working by adding auto layout constraints:
But I am not happy with this. Took a lot of trial and error and couldn't understand why this worked.
Also I had to add to use titleLabel.numberOfLines = 0 in my ViewController
I know it's a bit old but since I recently looked into it :
let l = UILabel()
l.numberOfLines = 0
l.lineBreakMode = .ByWordWrapping
l.text = "BLAH BLAH BLAH BLAH BLAH"
l.frame.size.width = 300
l.sizeToFit()
First set the numberOfLines property to 0 so that the device understands you don't care how many lines it needs.
Then specify your favorite BreakMode
Then the width needs to be set before sizeToFit() method. Then the label knows it must fit in the specified width
This is much better approach if you are looking for multiline dynamic text label which exactly takes the space based on its text.
No sizeToFit, preferredMaxLayoutWidth used
Below is how it will work.
Lets set up the project. Take a Single View application and in Storyboard Add a UILabel and a UIButton. Define constraints to UILabel as below snapshot:
Set the Label properties as below image:
Add the constraints to the UIButton. Make sure that vertical spacing of 100 is between UILabel and UIButton
Now set the priority of the trailing constraint of UILabel as 749
Now set the Horizontal Content Hugging and Horizontal Content Compression properties of UILabel as 750 and 748
Below is my controller class. You have to connect UILabel property and Button action from storyboard to viewcontroller class.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textLabel: UILabel!
var count = 0
let items = ["jackson is not any more in this world", "Jonny jonny yes papa eating sugar no papa", "Ab", "What you do is what will happen to you despite of all measures taken to reverse the phenonmenon of the nature"]
#IBAction func updateLabelText(sender: UIButton) {
if count > 3 {
count = 0
}
textLabel.text = items[count]
count = count + 1
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//self.textLabel.sizeToFit()
//self.textLabel.preferredMaxLayoutWidth = 500
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Thats it. This will automatically resize the UILabel based on its content and also you can see the UIButton is also adjusted accordingly.
It should work. Try this
var label:UILabel = UILabel(frame: CGRectMake(10
,100, 300, 40));
label.textAlignment = NSTextAlignment.Center;
label.numberOfLines = 0;
label.font = UIFont.systemFontOfSize(16.0);
label.text = "First label\nsecond line";
self.view.addSubview(label);
With Graphical User Interface (GUI) in Xcode, you can do the following:
Go to "Attribute Inspector" and set Lines value to 0. By default, it is set to 1.
The Label text can be written in multi-line by hitting option + return.
Now, go to "Size Inspector" and set the width, height, X & Y position of the Label.
That's all.
Programmatically, Swift
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.titleView.numberOfLines = 2
Programmatically in Swift 5 with Xcode 10.2
Building on top of #La masse's solution, but using autolayout to support rotation
Set anchors for the view's position (left, top, centerY, centerX, etc). You can also set the width anchor or set the frame.width dynamically with the UIScreen extension provided (to support rotation)
label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
self.view.addSubview(label)
// SET AUTOLAYOUT ANCHORS
label.translatesAutoresizingMaskIntoConstraints = false
label.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 20).isActive = true
label.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -20).isActive = true
label.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20).isActive = true
// OPTIONALLY, YOU CAN USE THIS INSTEAD OF THE WIDTH ANCHOR (OR LEFT/RIGHT)
// label.frame.size = CGSize(width: UIScreen.absoluteWidth() - 40.0, height: 0)
label.text = "YOUR LONG TEXT GOES HERE"
label.sizeToFit()
If setting frame.width dynamically using UIScreen:
extension UIScreen { // OPTIONAL IF USING A DYNAMIC FRAME WIDTH
class func absoluteWidth() -> CGFloat {
var width: CGFloat
if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
width = self.main.bounds.height // Landscape
} else {
width = self.main.bounds.width // Portrait
}
return width
}
}
extension UILabel {
var textSize: CGSize { text?.size(withAttributes: [.font: font!]) ?? .zero }
func setSizeForText(_ str: String, maxWidth: CGFloat) {
text = str
let dividedByMaxWidth = Int(textSize.width / maxWidth)
if dividedByMaxWidth == 0 {
frame.size = textSize
} else {
numberOfLines = dividedByMaxWidth + 1
frame.size = CGSize(width: maxWidth, height: frame.size.height * CGFloat(numberOfLines))
sizeToFit()
}
}
}
sizeToFit() in the end will shrink the label's width to the widest line after word break
This has worked for me:
Set the numberOfLines property of UILabel to 0
add this line: yourLabel.sizeToFit() after assigning text to the UILabel

Resources