How to get CGFloat/CGSize based on String length? - ios

I have a UIView, which acts as a container and the width of this container needs to change dynamically based on the longest String. So I want to get CGFloat/CGSize based on String length, is it still possible ?
The method which seems most helpful is now depreciated...
myStringSize : CGSize = [longestString sizeWithFont:myFont
constrainedToSize:maximumSize
lineBreakMode:self.myLabel.lineBreakMode];
Rather than basing it on a label I would like to base it on a String

You have to give Known Width or Height:
extension String {
func getHeight(with width: CGFloat, font: UIFont) -> CGFloat {
let maxSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
let actualSize = self.boundingRect(with: maxSize, options: [.usesLineFragmentOrigin], attributes: [NSFontAttributeName: font], context: nil)
return actualSize.height
}
func getWidth(with height: CGFloat, font: UIFont) -> CGFloat {
let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: height)
let actualSize = self.boundingRect(with: maxSize, options: [.usesLineFragmentOrigin], attributes: [NSFontAttributeName: font], context: nil)
return actualSize.width
}
}

1 - First OF all define The Max Size That you want MaxWidth
CGSize constraint = CGSizeMake(MaxWidth, CGFLOAT_MAX);
2- attributes for Your text :
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys: [UIFont systemFontOfSize:FONT_SIZE], NSFontAttributeName, nil];
3-* This is How to get The paraRect From text:
CGRect paraRect = [text boundingRectWithSize:constraint options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:attributes context:nil];
4-* Finally Get The Size :
CGSize size = paraRect.size;

Thank you for your help #Venkat, this is the solution that worked for me
#objc func containerWidth() -> (CGFloat){
let containerSize = self.myLabel.intrinsicContentSize
containerWidth = containerSize.width
return containerWidth
}

Related

get height of dynamic textViews in tableView without using automatic dimensions using swift

I want to calculate the height of my textView's in a tableView dynamically to use in heightForRowAt. I DO NOT want to use automatic dimensions as it often messes up my scrolling in containerViews.
Presently I am instantiating a textView for every cell, adding the text and getting the height using:
var textViewForCellHeight = UITextView()
textViewForCellHeight.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
textViewForCellHeight.frame = CGRect(x: 0, y: 0, width: self.tableView.frame.size.width - cellHorizontalPadding - tableView.safeAreaInsets.left - tableView.safeAreaInsets.right, height: 0)
textViewForCellHeight.text = myString
textViewForCellHeight.sizeToFit()
return textViewForCellHeight.frame.size.height
Using this in heightForRowAt works fine and gives the correct height for the cell, but it expensive and slows down the tableView considerably. Is there a more efficient way to get the height of the tableView cell's dynamically with a textView?
You can simply pass your string in this function and you will get dynamic height according to your string.
func calculateHeight(inString:String) -> CGFloat
{
let messageString = input.text
let attributes : [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 15.0)]
let attributedString : NSAttributedString = NSAttributedString(string: messageString!, attributes: attributes)
let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)
let requredSize:CGRect = rect
return requiredSize.height
}
Try this code:
func cellHeight(withTxt string: String) -> Float {
let textRect: CGRect = string.boundingRect(with: CGSize(width: self.view.frame.width/* Preferred textView Width */, height: CGFloat(MAXFLOAT)), options: ([.usesLineFragmentOrigin, .usesFontLeading]), attributes: [.font: UIFont(name: "Helvetica Neue", size: 17)!], context: nil)
let requiredSize: CGSize = textRect.size
//finally u return your height
return Float(requiredSize.height)
}

Calculating attributed string height

I'm trying to set a tableViewCell height equal to the height of the attributedString inside the cell. However whatever I do it does not seem to have the correctSize. this is what I've tried so far
cellHeight
//Convert description to NSAttributedString and get height
//width is equal to screen width - right and left offset
//at the end add the bottom and height offset to the cell height
return detailPetViewModel!.description
.lineSpacing(spacing: 4)
.heightWithConstrainedWidth(width: Sizes.screenWidth - 40) + 10
Customize descLabel in cell subclass
//Customize descLabel
descLabel.font = FontFamily.Avenir.Regular.font(size: 16)
descLabel.textColor = UIColor(named: .SecondaryTextColor)
//Multiple lines
descLabel.numberOfLines = 0
descLabel.lineBreakMode = .byWordWrapping
descLabel.sizeToFit()
linespacing extension
extension String {
func lineSpacing(spacing: CGFloat) -> NSAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = spacing
let attributedString = NSMutableAttributedString(string: self)
attributedString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
return attributedString
}
}
height Extension
extension NSAttributedString {
func heightWithConstrainedWidth(width: CGFloat) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
return boundingBox.height
}
}
This is the extension I use. You shouldn't have to pass in any information about the font or line-height as the attributed string already knowns its values.
extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return ceil(rect.size.height)
}
func width(containerHeight: CGFloat) -> CGFloat {
let rect = self.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: containerHeight),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return ceil(rect.size.width)
}
}
Used like this:
let height = detailPetViewModel!.description.height(containerWidth: Sizes.screenWidth - 40 + 10)
You can simply use (the instance of NSAttributedString).size.height
e.g.
let a = NSAttributedString(string: "A string", attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14)])
let height = a.size().height

Wrong text height when text contains emoji

Following the official docs, I created this function to calculate text height.
func calculateTextHeight(myString: String, myWidth: CGFloat, myFont: UIFont) -> CGFloat {
let textStorage = NSTextStorage(string: myString)
let textContainer = NSTextContainer(size: CGSize(width: myWidth, height: CGFloat.max))
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textStorage.addAttribute(NSFontAttributeName, value: myFont, range: NSMakeRange(0, textStorage.length))
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .ByWordWrapping
layoutManager.glyphRangeForTextContainer(textContainer)
return layoutManager.usedRectForTextContainer(textContainer).size.height
}
But the calculated height is wrong when the text contains an emoji.
var s = "ABCDE 12345"
print(calculateTextHeight(s, myWidth: 500, myFont: UIFont.systemFontOfSize(14)))
// prints 16.7 (correct)
s = "ABCDE 12345 💩"
print(calculateTextHeight(s, myWidth: 500, myFont: UIFont.systemFontOfSize(14)))
// prints 22.9 (should be 16.7)
Is this a bug? How can I fix this?
This worked for me when text contains emojis. For NSAttributedString strings:
extension NSAttributedString {
func sizeOfString(constrainedToWidth width: Double) -> CGSize {
let framesetter = CTFramesetterCreateWithAttributedString(self)
return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0, length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil)
}
}
For String:
extension String {
func sizeOfString(constrainedToWidth width: Double, font: UIFont) -> CGSize {
let attributes = [NSAttributedString.Key.font : font]
let attString = NSAttributedString(string: self, attributes: attributes)
let framesetter = CTFramesetterCreateWithAttributedString(attString)
return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0, length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil)
}
}
I used an alternate method to calculate text height. This works with emojis.
static func calculateStringHeight(str: String, maxWidth: CGFloat, font: UIFont) -> CGFloat {
return str.boundingRectWithSize(CGSizeMake(maxWidth, CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil).height
}
I do not think it is a bug. Emoji take up more space to be displayed.
I believe this will make a difference only if the number of emoji in your text is too large.
If you try the code below, i think that the result will be the same.
s = "ABCDE 12345 💩💩💩💩💩💩💩"
print(calculateTextHeight(s, myWidth: 500, myFont: UIFont.systemFontOfSize(14)))
// prints 22.9
If you want to eliminate the emoji, you can remove them from the original text before doing the height calculation.
In this case, you need to scan the original text by replacing all emoji by other character, and then call the height calculation.

WKInterfacelabel check if truncated

I would like to check if the text in WKInterfacelabel is truncated. Since there is no text property and it seems I can not get the number of lines property that you can set in storyboard I wonder if there is any way or trick on how to achieve this!?
Thanks !
You can get the number of lines that a label takes to display given text using below code.
CGFloat labelWidth = 100.0f;
NSString *text = #"some text";
[self.label setText:text];
[self.label setWidth:labelWidth];
UIFont *font = [UIFont systemFontOfSize:12.0f];
CGRect rect = [text boundingRectWithSize:CGSizeMake(labelWidth, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName : font}
context:nil];
CGFloat numOfLines = ceil(rect.size.height / font.lineHeight);
Swift version of #Sahana's answer above
func isTruncated(text: String, width: CGFloat, font: UIFont, numOfLines: Int) -> Bool {
let rect = text.boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)),
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font : font],
context: nil)
let thisNumOfLines = Int(ceil(rect.size.height / font.lineHeight))
let isTruncated = thisNumOfLines > numOfLines
return isTruncated
}

How to find actual number of lines of UILabel?

How can I find the actual number of lines of a UILabel after I have initialized it with a text and a font? I have set its numberOfLines property to 0, so it will expand to however many lines are necessary. But then, how can I find out how many lines it finally got after I set its text?
I found similar questions, but none seems to provide a concise answer and it seems to me that it must be really easy to get it without any overhead on juggling around with the boundingRectWithSize or sizeWithFont,...
None of these worked for me. Below one did,
Swift 4.2:
extension UILabel {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
let charSize = font.lineHeight
let text = (self.text ?? "") as NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
}
Swift 4/4.1:
extension UILabel {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
let charSize = font.lineHeight
let text = (self.text ?? "") as NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
}
Swift 3:
extension UILabel {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
let charSize = font.lineHeight
let text = (self.text ?? "") as NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
}
Swift 5 (IOS 12.2)
Get max number of lines required for a label to render the text without truncation.
extension UILabel {
var maxNumberOfLines: Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(MAXFLOAT))
let text = (self.text ?? "") as NSString
let textHeight = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
let lineHeight = font.lineHeight
return Int(ceil(textHeight / lineHeight))
}
}
Get max number of lines can be displayed in a label with constrained bounds. Use this property after assigning text to label.
extension UILabel {
var numberOfVisibleLines: Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(MAXFLOAT))
let textHeight = sizeThatFits(maxSize).height
let lineHeight = font.lineHeight
return Int(ceil(textHeight / lineHeight))
}
}
Usage
print(yourLabel.maxNumberOfLines)
print(yourLabel.numberOfVisibleLines)
Firstly set text in UILabel
First Option :
Firstly calculate height for text according to font :
NSInteger lineCount = 0;
CGSize labelSize = (CGSize){yourLabel.frame.size.width, MAXFLOAT};
CGRect requiredSize = [self boundingRectWithSize:labelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: yourLabel.font} context:nil];
Now calculate number of lines :
int charSize = lroundf(yourLabel.font.lineHeight);
int rHeight = lroundf(requiredSize.height);
lineCount = rHeight/charSize;
NSLog(#"No of lines: %i",lineCount);
Second Option :
NSInteger lineCount = 0;
CGSize textSize = CGSizeMake(yourLabel.frame.size.width, MAXFLOAT);
int rHeight = lroundf([yourLabel sizeThatFits:textSize].height);
int charSize = lroundf(yourLabel.font.lineHeight);
lineCount = rHeight/charSize;
NSLog(#"No of lines: %i",lineCount);
Here is a swift version of #Paresh solution:
func lines(label: UILabel) -> Int {
let textSize = CGSize(width: label.frame.size.width, height: CGFloat(Float.infinity))
let rHeight = lroundf(Float(label.sizeThatFits(textSize).height))
let charSize = lroundf(Float(label.font.lineHeight))
let lineCount = rHeight/charSize
return lineCount
}
EDIT: I don't know why, but the code is returning 2 more lines than the actual number of lines, for my solution, I just subtracted them before returning lineCount.
Swift 5.2
The main point to make it work for me was to call label.layoutIfNeeded() because I was using autoLayout, otherwise it doesnt work.
func actualNumberOfLines(label: UILabel) -> Int {
// You have to call layoutIfNeeded() if you are using autoLayout
label.layoutIfNeeded()
let myText = label.text! as NSString
let rect = CGSize(width: label.bounds.width, height: CGFloat.greatestFiniteMagnitude)
let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: label.font as Any], context: nil)
return Int(ceil(CGFloat(labelSize.height) / label.font.lineHeight))
}
Credits to: https://gist.github.com/fuxingloh/ccf26bb68f4b8e6cfd02, which provided the solution in an older swift version, and for mentioning the importance of layoutIfNeeded().
The other answers here don't respect the numberOfLines property of UILabel when it is set to something other than 0.
Here's another option you can add to your category or subclass:
- (NSUInteger)lineCount
{
CGSize size = [self sizeThatFits:CGSizeMake(self.frame.size.width, CGFLOAT_MAX)];
return MAX((int)(size.height / self.font.lineHeight), 0);
}
Some notes:
I'm using this on a UILabel with attributed text, without ever actually setting the font property, and it's working fine. Obviously you would run into issues if you were using multiple fonts in your attributedText.
If you are subclassing UILabel to have custom edge insets (for example by overriding drawTextInRect:, which is a neat trick I found here), then you must remember to take those insets into account when calculating the size above. For example: CGSizeMake(self.frame.size.width - (self.insets.left + self.insets.right), CGFLOAT_MAX)
Here is the Swift3 Code
here you can define Int value and get the height of text size by using (MAXFLOAT) and using that height you can get the total height of UILabel and by deviding that total height by character size you can get the actual line count of UILabel.
var lineCount: Int = 0
var textSize = CGSize(width: CGFloat(yourLabel.frame.size.width), height: CGFloat(MAXFLOAT))
var rHeight: Int = lroundf(yourLabel.sizeThatFits(textSize).height)
var charSize: Int = lroundf(yourLabel.font.leading)
lineCount = rHeight / charSize
print("No of lines: \(lineCount)")
It seems that the official developer website mentions one solution Counting Lines of Text in Objc. However, it assumes you have a reference to a text view configured with a layout manager, text storage, and text container. Unfortunately, UILabel doesn't expose those to us, so we need create them with the same configuration as the UILabel.
I translated the Objc code to swift as following. It seems work well for me.
extension UILabel {
var actualNumberOfLines: Int {
let textStorage = NSTextStorage(attributedString: self.attributedText!)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: self.bounds.size)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = self.lineBreakMode
layoutManager.addTextContainer(textContainer)
let numberOfGlyphs = layoutManager.numberOfGlyphs
var numberOfLines = 0, index = 0, lineRange = NSMakeRange(0, 1)
while index < numberOfGlyphs {
layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
index = NSMaxRange(lineRange)
numberOfLines += 1
}
return numberOfLines
}
}
You can find the total number of line available in your custom label
Please check this code...
NSInteger numberOfLines = [self lineCountForText:#"YOUR TEXT"];
- (int)lineCountForText:(NSString *) text
{
UIFont *font = [UIFont systemFontOfSize: 15.0];
int width=Your_LabelWidht;
CGRect rect = [text boundingRectWithSize:CGSizeMake(width, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName : font} context:nil];
return ceil(rect.size.height / font.lineHeight);
}
Following up on #Prince's answer, I now implemented a category on UILabel as follows (note that I corrected some minor syntax mistakes in his answer that wouldn't let the code compile):
UILabel+Util.h
#import <UIKit/UIKit.h>
#interface UILabel (Util)
- (NSInteger)lineCount;
#end
UILabel+Util.,
#import "UILabel+Util.h"
#implementation UILabel (Util)
- (NSInteger)lineCount
{
// Calculate height text according to font
NSInteger lineCount = 0;
CGSize labelSize = (CGSize){self.frame.size.width, FLT_MAX};
CGRect requiredSize = [self.text boundingRectWithSize:labelSize options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName: self.font} context:nil];
// Calculate number of lines
int charSize = self.font.leading;
int rHeight = requiredSize.size.height;
lineCount = rHeight/charSize;
return lineCount;
}
#end
Xamarin iOS
label.Text = text;
var lineCount = 0;
var textSize = new CGSize(label.Frame.Size.Width, float.MaxValue);
var height = label.SizeThatFits(textSize).Height;
var fontHeight = label.Font.LineHeight;
lineCount = Convert.ToInt32(height / fontHeight);
Note that #kurt-j's answer will not always work. In some cases, you will have to manually provide the width of label. Since these cases exist, it is a good idea to have an optional width parameter, even if you don't end up using it.
Swift 4.2:
extension UILabel {
func calculateMaxLines(actualWidth: CGFloat?) -> Int {
var width = frame.size.width
if let actualWidth = actualWidth {
width = actualWidth
}
let maxSize = CGSize(width: width, height: CGFloat(Float.infinity))
let charSize = font.lineHeight
let text = (self.text ?? "") as NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
}
let l = UILabel()
l.numberOfLines = 0
l.layer.frame.size.width = self.view.frame.width - 40 /*padding(20 + 20)*/
l.font = UIFont(name: "BwModelica-Bold", size: 16.0)
l.text = "Random Any length Text!!"
let noOfLines = ceil(l.intrinsicContentSize.width / l.frame.size.width)
let lbl_height = noOfLines * l.intrinsicContentSize.height
This will be your Exact dynamic height of Label and Number of lines. Happy coding!!!
Xamarin.iOS
Thanks to the answers everyone provided above.
This gets number of visible lines.
public static int VisibleLineCount(this UILabel label)
{
var textSize = new CGSize(label.Frame.Size.Width, nfloat.MaxValue);
nfloat rHeight = label.SizeThatFits(textSize).Height;
nfloat charSize = label.Font.LineHeight;
return Convert.ToInt32(rHeight / charSize);
}
This gets actual number of lines the text will occupy on screen.
public static int LineCount(this UILabel label)
{
var maxSize = new CGSize(label.Frame.Size.Width, nfloat.MaxValue);
var charSize = label.Font.LineHeight;
var text = (label.Text ?? "").ToNSString();
var textSize = text.GetBoundingRect(maxSize, NSStringDrawingOptions.UsesLineFragmentOrigin, new UIStringAttributes() { Font = label.Font }, null);
return Convert.ToInt32(textSize.Height / charSize);
}
A helper method I find useful for my use case.
public static bool IsTextTruncated(this UILabel label)
{
if (label.Lines == 0)
{
return false;
}
return (label.LineCount() > label.Lines);
}
To get a more accurate line count:
Use font.lineHeight instead of font.pointSize
round() the line count after division
Swift 5.4
Refactor solution of Fernando Cardenas to UILabel extension
private extension UILabel {
var actualNumberOfLines: Int {
guard let text = self.text else {
return 0
}
layoutIfNeeded()
let rect = CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
let labelSize = text.boundingRect(
with: rect,
options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font as Any],
context: nil)
return Int(ceil(CGFloat(labelSize.height) / font.lineHeight))
}
}
⚠️ Nor lineHeight, nor leading is sufficient on its own.
The font.lineHeight is the height of the glyphs, it spans from descender (the bottom of the lowest glyph) to ascender (the top of the highest glyph). Also, note that lineHeight can be override for an attributed string. The font.leading is the additional (may be negative, though) space between (!) the lines (see docs for yourself). If you use the system fonts, you get different leading values for almost every point size.
So e.g. the height of a label with 5 lines is consist of 5 lineHeight and 4 leading (yet the leading value is often small enough to make the above solutions work up until a point where you start to work with a multitude of lines.
So the correct (pseudo) formula should be:
(frame.size.height + font.leading) / (font.lineHeight + font.leading)
Also also, if the attributed string has an attachment that is too big (higher than the ascender of the font), then it also alters the line height for that row.

Resources