Aligning glyphs to the top of a UITextView after sizeToFit - ios

The app I'm working on supports hundreds of different fonts. Some of these fonts, particularly the script fonts, have significant ascenders and descenders. When sizeToFit() is called on a UITextView with some of these fonts, I end up with significant top and bottom padding (the image on the left). The goal is to end up with the image on the right, such that the tallest glyph is aligned flush with the top of the text view's bounding box.
Here's the log for the image above:
Point Size: 59.0
Ascender: 70.21
Descender: -33.158
Line Height: 103.368
Leading: 1.416
TextView Height: 105.0
My first thought was to look at the height of each glyph in the first line of text, and then calculate the offset between the top of the container and the top of the tallest glyph. Then I could use textContainerInset to adjust the top margin accordingly.
I tried something like this in my UITextView subclass:
for location in 0 ..< lastGlyphIndexInFirstLine {
let glphyRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: location, length: 1), in: self.textContainer)
print(glphyRect.size.height) // prints 104.78399999999999 for each glyph
}
Unfortunately, this doesn't work because boundRect(forGlyphRange:in:) doesn't appear to return the rect of the glyph itself (I'm guessing this is always the same value because it's returning the height of the line fragment?).
Is this the simplest way to solve this problem? If it is, how can I calculate the distance between the top of the text view and the top of the tallest glyph in the first line of text?

This doesn't appear to be possible using TextKit, but it is possible using CoreText directly. Specifically, CGFont's getGlyphBBoxes returns the correct rect in glyph space units, which can then be converted to points relative to the font size.
Credit goes to this answer for making me aware of getGlyphBBoxes as well as documenting how to convert the resulting rects to points.
Below is the complete solution. This assumes you have a UITextView subclass with the following set beforehand:
self.contentInset = .zero
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0.0
This function will now return the distance from the top of the text view's bounds to the top of the tallest used glyph:
private var distanceToGlyphs: CGFloat {
// sanity
guard
let font = self.font,
let fontRef = CGFont(font.fontName as CFString),
let attributedText = self.attributedText,
let firstLine = attributedText.string.components(separatedBy: .newlines).first
else { return 0.0 }
// obtain the first line of text as an attributed string
let attributedFirstLine = attributedText.attributedSubstring(from: NSRange(location: 0, length: firstLine.count)) as CFAttributedString
// create the line for the first line of attributed text
let line = CTLineCreateWithAttributedString(attributedFirstLine)
// get the runs within this line (there will typically only be one run when using a single font)
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
guard let runs = glyphRuns as? [CTRun] else { return 0.0 }
// this will store the maximum distance from the baseline
var maxDistanceFromBaseline: CGFloat = 0.0
// iterate each run
for run in runs {
// get the total number of glyphs in this run
let glyphCount = CTRunGetGlyphCount(run)
// initialize empty arrays of rects and glyphs
var rects = Array<CGRect>(repeating: .zero, count: glyphCount)
var glyphs = Array<CGGlyph>(repeating: 0, count: glyphCount)
// obtain the glyphs
self.layoutManager.getGlyphs(in: NSRange(location: 0, length: glyphCount), glyphs: &glyphs, properties: nil, characterIndexes: nil, bidiLevels: nil)
// obtain the rects per-glyph in "glyph space units", each of which needs to be scaled using units per em and the font size
fontRef.getGlyphBBoxes(glyphs: &glyphs, count: glyphCount, bboxes: &rects)
// iterate each glyph rect
for rect in rects {
// obtain the units per em from the font ref so we can convert the rect
let unitsPerEm = CGFloat(fontRef.unitsPerEm)
// sanity to prevent divide by zero
guard unitsPerEm != 0.0 else { continue }
// calculate the actual distance up or down from the glyph's baseline
let glyphY = (rect.origin.y / unitsPerEm) * font.pointSize
// calculate the actual height of the glyph
let glyphHeight = (rect.size.height / unitsPerEm) * font.pointSize
// calculate the distance from the baseline to the top of the glyph
let glyphDistanceFromBaseline = glyphHeight + glyphY
// store the max distance amongst the glyphs
maxDistanceFromBaseline = max(maxDistanceFromBaseline, glyphDistanceFromBaseline)
}
}
// the final top margin, calculated by taking the largest ascender of all the glyphs in the font and subtracting the max calculated distance from the baseline
return font.ascender - maxDistanceFromBaseline
}
You can now set the text view's top contentInset to -distanceToGlyphs to achieve the desired result.

Related

How do you get correct caret size AND position when using NSMutableParagraphStyle and paragraphSpacingBefore in Swift 4

I've been playing around with attributed text in a UITextView (Swift 4.2 and noticed that once I introduced "paragraphSpacingBefore" into my design, the Caret becmae too large on the first line of each new paragraph.
I found this suggested fix on Stackoverflow which seemed to work ok to fix the caret size. The problem I found was the caret itself floats above the target line when that line was the start of a new paragraph.
UITextView lineSpacing make cursor height not same
Caret Floats above the target line
I tried solving it, maintaining the core idea of the original solution and adding some offset logic. During debugging I noticed that the original answer for caret size always adjusts the size even when not required so I added a variance filter (only adjust if variance > 10%). Did this because I think adjusting every time will interfere with my soln. to the floating caret problem.
If someone can take a look at my proposed approach, suggest improvements or a better way etc i'd be grateful:
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else {
return superRect
}
let proposedHeight: CGFloat = isFont.pointSize - isFont.descender
var delta: CGFloat = superRect.size.height - proposedHeight
delta = (delta * delta).squareRoot()
//If the delta is < 10% of the original height just return the original rect
if delta / superRect.size.height < 0.1 {
return superRect
}
superRect.size.height = isFont.pointSize - isFont.descender
// "descender" is expressed as a negative value,
// so to add its height you must subtract its value
superRect.origin.y = superRect.origin.y + delta
// delta is used to correct for resized caret floating above the target line
return superRect
}
I got a solution:
// Fix long cursor height when at the end of paragraph with paragraphspacing and wrong cursor position in titles with paragraph spacing before
override public func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else { return superRect }
let location = self.offset(from: self.beginningOfDocument, to: position)
if let paragrahStyle = self.storage.attribute(.paragraphStyle, at: location, effectiveRange: nil) as? NSParagraphStyle {
superRect.origin.y += paragrahStyle.paragraphSpacingBefore
}
superRect.size.height = isFont.pointSize - isFont.descender
return superRect
}
The real problem paragraphSpacingBefore. So all you have to do is to get the paragraph styling attributes, get the spacing and move the cursor by that spacing. This works well with all the text.

Get all Glyphs of a UIFont that have a descender

Is there a way to get all the glyphs of a UIFont that contain a true descender? It seems that using CTLineGetTypographicBounds is not accurate and returns the exact same descent value for every line. I thought it would provide the information that I needed but it did not. So now I am looking to see if I can build a character set from the glyphs that contain true descenders unless there is another way. The ultimate goal would be able to see if a line of text is below the baseline.
let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attr))
//let's get the real descent test
var a : CGFloat = 0
var d : CGFloat = 0
var l : CGFloat = 0
let bounds = CTLineGetTypographicBounds(line, &a, &d, &l)
print("the descent is \(d)")
print("the ascent is \(a)")
print("the leading is \(l)")
Since it seems that your actual goal is to determine whether a string contains a character with a descender, you can use Core Text to look at the bounding rect of each glyph. If the bounding rect's origin in negative, this means the glyph starts below the baseline. This will be true for characters such as y but also ,.
func checkDescender(string: String) {
let uiFont = UIFont.systemFont(ofSize: 14) // Pick your font
let font = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
for ch in string.unicodeScalars {
let utf16codepoints = Array(ch.utf16)
var glyphs: [CGGlyph] = [0, 0]
let hasGlyph = CTFontGetGlyphsForCharacters(font, utf16codepoints, &glyphs, utf16codepoints.count)
if hasGlyph {
let rect = CTFontGetBoundingRectsForGlyphs(font, .default, glyphs, nil, 1)
// print("\(ch) has bounding box of \(rect)")
if rect.origin.y < 0 {
print("\(ch) goes below the baseline by \(-rect.origin.y)")
}
}
}
}
checkDescender(string: "Ymy,")
You might want to add additional checks to only look at letters depending on your needs.

Calculate width and height of AttributeText does not correct

I'm making an app chat and I have a problem with the chat message's size.
I've calculate my message's height like this and I've got the correct height
maximumMessageWidth = 300.0
let size = CGSize(width: maximumMessageWidth, height: 1000.0)
let messageAttributeText = messageBody.attributedText
let height = messageAttributeText?.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil).height
To avoid the special case like this image. I have to calculate the width
It has the big space after my message
I want it to look like this:
This is the code I use to calculate the message width (I used the correct height which I've calculated the width)
let size = CGSize(width: 1000.0, height: height)
let messageAttributeText = messageBody.attributedText
var width = messageAttributeText?.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil).width
but width in this code seem not right. Because I think it does care about my height property. It assume that my text is in only 1 line.
But I want to calculate the width, so that my text will `fill the whole label' just like the second image
Does anyone know how to calculate the width in my case ?
You can do that by casting String to NSString
var string = "Hello, World"
let nsString = string as NSString
let size = nsString.size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14)])
The size will give you width and height of the string.
You can apply this to your NSAttributedStrings string property.
After a bit of research, I find out this:
The calculate width code doesn't work right because it do not care about the height property.
If height = 100 or height = 1000 it will return the same result because it assume to draw all the text in just one line.
So to calculate the width, I used binary search
var minWidth:CGFloat = 0.0
var maxWidth = maxWidth
while true {
if (minWidth >= maxWidth) {
width = minWidth
break
}
let testWidth = (maxWidth + minWidth) / 2.0
if calculateMessageHeight(width: testWidth) > messageHeight {
minWidth = testWidth + 2.0
continue
} else {
maxWidth = testWidth - 2.0
continue
}

How to remove labels from after drawing with drawrect()

I am pretty new to drawing programmatically. I've managed to draw a line graph, complete with points, lines, auto-scaling axes, and axis labels. In other screens, I can change characteristics of the graph and when I return to the screen, I refresh it with setNeedsDisplay() in the viewWillAppear function of the containing viewController. The lines are redrawn perfectly when I do this.
The new data that is added in other screens may require rescaling the axes. The problem is that when the graph is redrawn, the number labels on the axes are just added to the graph, without removing the old ones, meaning that some labels may be overwritten, while some old ones just remain there next to the new ones.
I think I see why this happens, in that I am creating a label and adding a subview, but not removing it. I guess I figured that since the lines are erased and redrawn, the labels would be, too. How do I cleanly relabel my axes? Is there a better way to do this? My function for creating the labels is listed below. This function is called by drawRect()
func createXAxisLabels(interval: Float, numIntervals: Int) {
let xstart: CGFloat = marginLeft
let yval: CGFloat = marginTop + graphHeight + 10 // 10 pts below the x-axis
var xLabelVals : [Float] = [0]
var xLabelLocs : [CGFloat] = [] // gives the locations for each label
for i in 0...numIntervals {
xLabelLocs.append(xstart + CGFloat(i) * graphWidth/CGFloat(numIntervals))
xLabelVals.append(Float(i) * interval)
}
if interval >= 60.0 {
xUnits = "Minutes"
xUnitDivider = 60
}
for i in 0...numIntervals {
let label = UILabel(frame: CGRectMake(0, 0, 50.0, 16.0))
label.center = CGPoint(x: xLabelLocs[i], y: yval)
label.textAlignment = NSTextAlignment.Center
if interval < 1.0 {
label.text = "\(Float(i) * interval)"
} else {
label.text = "\(i * Int(interval/xUnitDivider))"
}
label.font = UIFont.systemFontOfSize(14)
label.textColor = graphStructureColor
self.addSubview(label)
}
}
drawRect should just draw the rectangle, not change the view hierarchy. It can be called repeatedly. Those other labels are other views and do their own drawing.
You have a couple of options
Don't use labels -- instead just draw the text onto the rect.
-or-
Add/remove the labels in the view controller.
EDIT (from the comments): The OP provided their solution
I just replaced my code inside the for loop with:
let str : NSString = "\(xLabelVals[i])"
let paraAttrib = NSMutableParagraphStyle()
paraAttrib.alignment = NSTextAlignment.Center
let attributes = [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.whiteColor(),
NSParagraphStyleAttributeName: paraAttrib]
let xLoc : CGFloat = CGFloat(xLabelLocs[i] - 25)
let yLoc : CGFloat = yval - 8.0
str.drawInRect(CGRectMake(xLoc, yLoc, 50, 16), withAttributes: attributes)

boundingRectWithSize:options:context: calculate size not consider NSParagraphStyle.firstLineHeadIndent

for example:
import Foundation
import UIKit
var str = NSString(string: "saldkjaskldjhf")
var font = UIFont.systemFontOfSize(14.0)
var attributes:[String:AnyObject] = [NSFontAttributeName: font]
var attriStrWithoutParagraph = NSAttributedString(string: str, attributes: attributes)
var size = attriStrWithoutParagraph.boundingRectWithSize(CGSize(width: CGFloat.max, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
var paragraphstyle = NSMutableParagraphStyle()
paragraphstyle.firstLineHeadIndent = CGFloat(20)
attributes[NSParagraphStyleAttributeName] = paragraphstyle
attriStrWithoutParagraph = NSAttributedString(string: str, attributes: attributes)
size = attriStrWithoutParagraph.boundingRectWithSize(CGSize(width: CGFloat.max, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
here is the output:
(0.0,0.0,87.276,16.702)
(0.0,0.0,87.276,16.702)
we can see the result is the same, so the firstlineindent is not considered in why it works like this???
You're specifying very large (effectively infinite) values (CGFloat.max) for the size that you're passing to -boundingRectWithSize:options:. So, the text will never wrap. It will always be laid out in one long line.
Furthermore, the docs for -boundingRectWithSize:options: say:
The origin of the rectangle returned from this method is the first glyph origin.
So, the result is always relative to where the first glyph is placed. You're basically measuring the size of the line. The indent doesn't change the size of the line. It changes where the first glyph is placed, but the result is relative to the first glyph, so it doesn't change the result.
It would change the result if you were providing a real limit for the width and making the paragraph wrap. In that case, the second line would be "outdented" relative to the first line (and the first glyph), so the bounding rectangle would change as you change the firstLineHeadIndent.
You can simply apply the desired indent yourself. That is, after you get the bounding rect, add the indent distance to the X coordinate of the origin (edit: or to the width, if you want a rect encompassing the indent and not just the text positioned by the indent). (Although it's not clear to me what it could mean to indent text in an "infinite" space.)
You could also provide an actual bounding size for your desired destination for the text.

Resources