Skip to content

Instantly share code, notes, and snippets.

@krzyzanowskim
Last active November 12, 2023 14:51
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save krzyzanowskim/e92eaf31e0419820c0f8cbcf96ba1269 to your computer and use it in GitHub Desktop.
Save krzyzanowskim/e92eaf31e0419820c0f8cbcf96ba1269 to your computer and use it in GitHub Desktop.
Calculate frame of String, that fits given width
// Excerpt from https://github.com/krzyzanowskim/CoreTextWorkshop
// Licence BSD-2 clause
// Marcin Krzyzanowski marcin@krzyzanowskim.com
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)
}
@Semty
Copy link

Semty commented Jan 29, 2022

@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!

@krzyzanowskim
Copy link
Author

It should be floor(value) + 0.5 for pixel aligning.

@krzyzanowskim
Copy link
Author

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.

@NikKovIos
Copy link

Missed bracket on 36 line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment