Hi everyone I have created a view that shows an error message. Within this view I have inserted a UILabel that shows the message. So far so good, the height of the UILabel changes based on the length of the text using text.boundingRect
My problem is that the text is shown correctly only if it does not exceed a certain number of lines, in case the text is too long it is cut and I don't understand why
In short, if the text is not very long I have no display problems otherwise if the text is very long it is cut
this is what i am using to get the height of the text.
Where am I wrong?
private func estimateTextHeight()-> CGFloat {
let text = (toastView.textLbl.text ?? "") as NSString
let attribute: [NSAttributedString.Key: Any] = [.font: toastView.textLbl.font!]
return text.boundingRect(with: .init(width: 300, height: 2000), options: .usesLineFragmentOrigin, attributes: attribute, context: nil).height
}
private func updateToast(icon: UIImage?) -> Void {
let height = estimateTextHeight()
toastView.heightAnchor.constraint(equalToConstant: height).isActive = true
}
To calculate label height try this:
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width,
height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [.font: font],
context: nil)
return ceil(boundingBox.height)
}
}
Related
I researched a bit and it looks like the default value for NSLayoutManager's .usesFontLeading value is true.
My problem:
I'm using boundingRect to get the size of the text in UILabel with a custom font, the height is correct if I remove .usesFontLeading but incorrect if I add that option.
My question:
In UILabel, does the font of my label automatically use my font's leading value? and secondly, am I required to set .usesFontLeading as an option in my boundingRect.
Try this:
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width,
height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: font],
context: nil)
return ceil(boundingBox.height)
}
}
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)
}
I want to get the length of some texts in UILabel, and change the width property of UILabel dynamically.
But I have no idea about how to set the parameters in the function boundingrect.
This is the documentation of Apple Developer.
func boundingRect(with size: CGSize,
options: NSStringDrawingOptions = [],
context: NSStringDrawingContext?) -> CGRect
,and I tried like this
let attr = NSMutableAttributedString(string: "test test test test , this is test text. test test test test , this is test text. test test test test , this is test text. ")
//attr.addattributes
print(attr.boundingrect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 0), option: .truncatesLastVisibleLine
,context: nil)!)
But finally I got false width in print,so why and how to get the correct one.
Use
public extension NSAttributedString {
public func width(height: CGFloat) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect,
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return ceil(boundingBox.height)
}
}
and then get let width = attr.width(height: height)
where height > 0
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
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.