How to get the height of a paragraph using PDFKit - ios

I am writing a pdf using iOS PDFKit. Typically I can get the height of a single text item such as a title by doing the following:
return titleStringRect.origin.y + titleStringRect.size.height
Where titleStringRect is the CGRect containing the string. The returned value is the y-coordinate for the bottom of that text so that I know where to start writing the next line of text.
I have not found a way to know where a paragraph ends. The solutions I have found have been to just make a big enough CGRect that the paragraph will definitely fit in.
I need to know exactly what the height of the CGRect should be based on the String that will be written into it. Here is my code:
func addParagraph(pageRect: CGRect, textTop: CGFloat, text: String) {
let textFont = UIFont(name: "Helvetica", size: 12)
let backupFont = UIFont.systemFont(ofSize: 12, weight: .regular)
// Set paragraph information. (wraps at word breaks)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// Set the text attributes
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont ?? backupFont
]
let attributedText = NSAttributedString(
string: text,
attributes: textAttributes
)
let textRect = CGRect(
x: 50.0,
y: textTop,
width: pageRect.width - 100,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
As you can see the above code just makes a CGRect that is 1/5th of the space below the previous text regardless of how many lines the paragraph will actually be.
I have tried averaging the character count per line in order to estimate how many lines the paragraph will be but this is unreliable and definitely a hack.
What I need is for the addParagraph function to return the y-coordinate for the bottom of the paragraph so that I know where to start writing the next piece of content.

I ended up finding the solution to this and it is pretty simple. I'll post the code and then explain it for anyone else who has this problem.
let paragraphSize = CGSize(width: pageRect.width - 100, height: pageRect.height)
let paragraphRect = attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
First define a CGSize that is a certain width and height. Set the width to the width you want the paragraph to be and set the height to a large value that will fit the content. Then call
attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
Where attributedText is the paragraph content. The boundingRect method returns a CGRect which is the size required to fit the content into, but no more. Now you can return the bottom of the paragraph. This method will not change the width unless it cannot fit the String into the height you provided. For my purpose this was perfect. Here is the full code:
func addParagraph(pageRect: CGRect, textTop: CGFloat, paragraphText: String) -> CGFloat {
let textFont = UIFont(name: "Helvetica", size: 12)
let backupFont = UIFont.systemFont(ofSize: 12, weight: .regular)
// Set paragraph information. (wraps at word breaks)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// Set the text attributes
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont ?? backupFont
]
let attributedText = NSAttributedString(
string: paragraphText,
attributes: textAttributes
)
// determine the size of CGRect needed for the string that was given by caller
let paragraphSize = CGSize(width: pageRect.width - 100, height: pageRect.height)
let paragraphRect = attributedText.boundingRect(with: paragraphSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
// Create a CGRect that is the same size as paragraphRect but positioned on the pdf where we want to draw the paragraph
let positionedParagraphRect = CGRect(
x: 50,
y: textTop,
width: paragraphRect.width,
height: paragraphRect.height
)
// draw the paragraph into that CGRect
attributedText.draw(in: positionedParagraphRect)
// return the bottom of the paragraph
return positionedParagraphRect.origin.y + positionedParagraphRect.size.height
}

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)
}

Vertically aligning NSTextAttachment in NSMutableAttributedString

I'm adding an icon to a UILabel using NSTextAttachment inside an NSMutableAttributedString like this:
//Setting up icon
let moneyIcon = NSTextAttachment()
moneyIcon.image = UIImage(named: "MoneyIcon")
let moneyIconString = NSAttributedString(attachment: moneyIcon)
//Setting up text
let balanceString = NSMutableAttributedString(string: " 1,702,200")
balanceString.insert(moneyIconString, at: 0)
//Adding string to label
self.attributedText = balanceString
self.sizeToFit()
But for some reason the icon isn't vertically aligned
Does anybody know how can I align it?
Thank you!
use bounds property of NSTextAttachment.
//Setting up icon
let moneyIcon = NSTextAttachment()
moneyIcon.image = UIImage(named: "MoneyIcon")
let imageSize = moneyIcon.image!.size
moneyIcon.bounds = CGRect(x: CGFloat(0), y: (font.capHeight - imageSize.height) / 2, width: imageSize.width, height: imageSize.height)
let moneyIconString = NSAttributedString(attachment: moneyIcon)
//Setting up text
let balanceString = NSMutableAttributedString(string: " 1,702,200")
balanceString.insert(moneyIconString, at: 0)
//Adding string to label
self.attributedText = balanceString
self.sizeToFit()
This answer, which is about vertically centering two differently sized fonts in a single NSAttributedString, mentions using the baseline offset to calculate the center of the string.
You can use the same approach when using an image:
Subtract the font size from the image's height and divide it by 2.
Subtract the font's descender from the value (since font size isn't the same as the ascent of your font). The font that you are particularly using (Baloo-Regular) has a descender value that differs from the standard and it should be divided by 2. Other fonts (including San Fransisco) don't need that fix or require a different divisor.
This code covers most cases, if your font behaves differently, you should check out the guide for managing texts in Text Kit.
// *Setting up icon*
let moneyIcon = NSTextAttachment()
// If you're sure a value is not and will never be nil, you can use "!".
// Otherwise, avoid it.
let moneyImage = UIImage(named: "MoneyIcon")!
moneyIcon.image = moneyImage
let moneyIconString = NSAttributedString(attachment: moneyIcon)
// *Setting up NSAttributedString attributes*
let balanceFontSize: CGFloat = 16
let balanceFont = UIFont(name: "Baloo", size: balanceFontSize)!
let balanceBaselineOffset: CGFloat = {
let dividend = moneyImage.size.height - balanceFontSize
return dividend / 2 - balanceFont.descender / 2
}()
let balanceAttr: [NSAttributedString.Key: Any] = [
.font: balanceFont,
.baselineOffset: balanceBaselineOffset
]
// *Setting up text*
let balanceString = NSMutableAttributedString(
string: " 1,702,200",
attributes: balanceAttr
)
balanceString.insert(moneyIconString, at: 0)

How to draw horizontally aligned NSAttributedString in Swift

I am trying to draw a multi-line string such that each line is horizontally aligned to the centre of the box. I am using NSAttributedString, thus:
let paraStyle = NSMutableParagraphStyle()
paraStyle.alignment = .center
textAttrs = [
NSFontAttributeName: font!,
NSForegroundColorAttributeName: UIColor.white,
NSParagraphStyleAttributeName: paraStyle,
NSBaselineOffsetAttributeName: NSNumber(floatLiteral: 0.0)
]
Then later I draw the string using NSAttributedString draw()
let uiText = NSAttributedString(string: text, attributes: textAttrs)
let point = CGPoint(x: self.bounds.width / 2 - uiText.size().width / 2, y: self.bounds.height / 2 - uiText.size().height / 2)
uiText.draw(at: point)
But it still comes out with each line aligned to the left. How can I draw a string in ios and center the alignment.
You may need to use draw(in rect: CGRect), rather than draw(at point: CGPoint).
override func draw(_ rect: CGRect) {
let paraStyle = NSMutableParagraphStyle()
paraStyle.alignment = .center
let textAttrs = [
NSFontAttributeName: UIFont.systemFont(ofSize: 17),
NSForegroundColorAttributeName: UIColor.blue,
NSParagraphStyleAttributeName: paraStyle,
NSBaselineOffsetAttributeName: NSNumber(floatLiteral: 0.0)
]
let text = "The American student who was released last week after being held in captivity for more than 15 months in North Korea has died, his family says. Otto Warmbier, 22, returned to the US last Tuesday, but it emerged he had been in a coma for a year.North Korea said botulism led to the coma, but a team of US doctors who assessed him dispute this account.Mr Warmbier was sentenced to 15 years of hard labour for attempting to steal a propaganda sign from a hotel."
let uiText = NSAttributedString(string: text, attributes: textAttrs)
uiText.draw(in: rect)
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 2
}

Dynamically set UILabel text alignment between .left and .justified

In my app I have a UILabel with two lines preset. I can set the text alignment to either .left or .justified.
If I set it to .left, there is no layout issue if there is enough space between the last word in a line and the maximum x position of the label. Yet, when there is not so much space, so that the last word is very near the maximum x position, it looks kinda weird, because it is not exactly right-aligned (as it would be with .justified.
If I set it to .justified, it is always aligned well, yet sometimes the distance between the individual characters looks weird.
What I'm looking for is a way to dynamically adjust the text alignment depending on the distance between the last word in the first line to the maximum x position of the label. Say, if the position of the last character of the last word is smaller than 50, I want to have text alignment .left, otherwise I'd like to have .justified. Is there any way on how to accomplish this?
I took a quite hacky approach which takes some processing power, but it seems to work.
First of all, I fetch the string in the first line of the label using this extension:
import CoreText
extension UILabel {
/// Returns the String displayed in the first line of the UILabel or "" if text or font is missing
var firstLineString: String {
guard let text = self.text else { return "" }
guard let font = self.font else { return "" }
let rect = self.frame
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(String(kCTFontAttributeName), value: CTFontCreateWithName(font.fontName as CFString, font.pointSize, nil), range: NSMakeRange(0, attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width + 7, height: 100))
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
guard let line = (CTFrameGetLines(frame) as! [CTLine]).first else { return "" }
let lineString = text[text.startIndex...text.index(text.startIndex, offsetBy: CTLineGetStringRange(line).length-2)]
return lineString
}
}
After that I calculate the width, a label with line number 1 and fixed height would require for that string using this extension:
extension UILabel {
/// Get required width for a UILabel depending on its text content and font configuration
class func calculateWidth(text: String, height: CGFloat, font: UIFont) -> CGFloat {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: height))
label.numberOfLines = 1
label.font = font
label.text = text
label.sizeToFit()
return label.frame.size.width
}
}
Based on that, I can calculate the distance to the right and decide whether to choose text alignment .left or .justified, so the main code looks like this:
// Set text
myLabel.text = someString
// Change text alignment depending on distance to right
let firstLineString = myLabel.firstLineString
let distanceToRight = myLabel.frame.size.width - UILabel.calculateWidth(text: firstLineString, height: myLabel.frame.size.height, font: myLabel.font)
myLabel.textAlignment = distanceToRight < 20 ? .justified : .left

Swift How to calculate one line text height from its font

I ran into an issue where I needed to animate translating a label vertically the same distance of a textField's text height. In most cases just textField.bounds.heigt but if the textField's height is bigger than the text height it will not be any good for me. So I need to know:
How to calculate the line height of the string text from its UIFont?
Regarding the duplicate:
There's a little bit different of what I need. that answer(which I've referenced in my answer) get the total height depending on 1) the string 2) the width 3) the font. What I needed is one line height dpending only on the font.
UIFont has a property lineHeight:
if let font = _textView.font {
let height = font.lineHeight
}
where font is your font
I have been searching for a way to do that and find this answer where it has a String extension to calculate the size for the string and a given font. I have modified it to do what I want (get the line height of text written using a font.):
extension UIFont {
func calculateHeight(text: String, width: CGFloat) -> CGFloat {
let constraintRect = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [NSAttributedStringKey.font: self],
context: nil)
return boundingBox.height
}
}
I hope this helps someone looking for it. (may be myself in the future).
I've used this String extension in the past to draw some text as opposed to creating a UILabel somewhere. I don't like the fact that I can't seem to get the real height of the specific text I want to draw (not every string contains capital letters or characters with descenders, etc.) I've used a couple of enums for horizontal and vertical alignment around the given point. Open to ideas on the vertical height.
public func draw(at pt: CGPoint,
font: UIFont? = UIFont.systemFont(ofSize: 12),
color: UIColor? = .black,
align: HorizontalAlignment? = .Center,
vAlign: VerticalAlignment? = .Middle)
{
let attributes: [NSAttributedString.Key : Any] = [.font: font!,
.foregroundColor: color!]
let size = self.boundingRect(with: CGSize(width: 0, height: 0),
options: [ .usesFontLeading ],
attributes: [ .font: font! ],
context: nil).size
var x = pt.x
var y = pt.y
if align == .Center {
x -= (size.width / 2)
} else if align == .Right {
x -= size.width
}
if vAlign == .Middle {
y -= (size.height / 2)
} else if vAlign == .Bottom {
y -= size.height
}
let rect = CGRect(x: x, y: y, width: size.width, height: size.height)
draw(in: rect, withAttributes: attributes)
}

Resources