Last active
November 12, 2023 14:51
-
-
Save krzyzanowskim/e92eaf31e0419820c0f8cbcf96ba1269 to your computer and use it in GitHub Desktop.
Calculate frame of String, that fits given width
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Excerpt from https://github.com/krzyzanowskim/CoreTextWorkshop | |
// Licence BSD-2 clause | |
// Marcin Krzyzanowski [email protected] | |
func getSizeThatFits(_ attributedString: NSAttributedString, maxWidth: CGFloat) -> CGSize { | |
let framesetter = CTFramesetterCreateWithAttributedString(attributedString) | |
let rectPath = CGRect(origin: .zero, size: CGSize(width: maxWidth, height: 50000)) | |
let ctFrame = CTFramesetterCreateFrame(framesetter, CFRange(), CGPath(rect: rectPath, transform: nil), nil) | |
guard let ctLines = CTFrameGetLines(ctFrame) as? [CTLine], !ctLines.isEmpty else { | |
return .zero | |
} | |
var ctLinesOrigins = Array<CGPoint>(repeating: .zero, count: ctLines.count) | |
// Get origins in CoreGraphics coodrinates | |
CTFrameGetLineOrigins(ctFrame, CFRange(), &ctLinesOrigins) | |
// Transform last origin to iOS coordinates | |
let transform: CGAffineTransform | |
#if os(macOS) | |
transform = CGAffineTransform.identity | |
#else | |
transform = CGAffineTransform(scaleX: 1, y: -1).concatenating(CGAffineTransform.init(translationX: 0, y: rectPath.height)) | |
#endif | |
guard let lastCTLineOrigin = ctLinesOrigins.last?.applying(transform), let lastCTLine = ctLines.last else { | |
return .zero | |
} | |
// Get last line metrics and get full height (relative to from origin) | |
var ascent: CGFloat = 0 | |
var descent: CGFloat = 0 | |
var leading: CGFloat = 0 | |
CTLineGetTypographicBounds(lastCTLine, &ascent, &descent, &leading) | |
let lineSpacing = (floor(ascent + descent + leading) * 0.2) + 0.5 // 20% by default, actual value depends on Paragraph | |
let lineHeight = floor(ascent + descent + leading) + 0.5 | |
// Calculate maximum height of the frame | |
let maxHeight = lastCTLineOrigin.y + descent + leading + (lineSpacing / 2) | |
return CGSize(width: maxWidth, height: maxHeight) | |
} |
It should be floor(value) + 0.5
for pixel aligning.
P.S. Personally I've been using NSAttributedString's boundingRect(with: options:) method for quite some time and it's been okay for the most part.
it's broken tho, it always has been. It works mostly, and when it fails - it depends on the font, characters, and given frames - depends on the context in general. I did use it by myself until it break the layout randomly, which pushed me to debug it further. You will find a bunch of questions about it eg. on StackOverflow.
Missed bracket on 36 line.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@krzyzanowskim hey, Marcin! From this snippet it's not quite clear whether a pixel-perfect drawing should be expressed like this:
floor(value) + 0.5
(source: https://youtu.be/GZqeYvu-KFc?t=2042)or like this:
floor((value) + 0.5)
Could you please explain what is your vision here? Because in the current snippet the lineSpacing variable uses the first one and the lineHeight variable uses the second one.
Thank you very much!
P.S. Personally I've been using NSAttributedString's
boundingRect(with: options:)
method for quite some time and it's been okay for the most part. Is there any reason to use this approach? Because I've run some tests and it seems that the former is more precise.P.P.S. Anyway, great job at the conference!