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