For a fully working solution, see the bottom of my answer 👇
To manually measure the dimensions of the text
/ attributedText
of your UILabel
in order to find the appropriate font size using your own strategy, you have a few options:
Use NSString
's size(withAttributes:)
or NSAttributedString
's size()
function. These are only partially useful because they assume the text is one line.
Use NSAttributedString
's boundingRect()
function, which takes a few drawing options, making sure you supply .usesLineFragmentOrigin
to support multiple lines:
var textToMeasure = label.attributedText
// Modify the font size in `textToMeasure` as necessary
// Now measure
let rect = textToMeasure.boundingRect(with: label.bounds, options: [. usesLineFragmentOrigin], context: nil)
Use TextKit and your own NSLayoutManager:
var textToMeasure = label.attributedText
// Modify the font size in `textToMeasure` as necessary
// Now measure
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude))
let textStorage = NSTextStorage(attributedString: string)
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
let glyphRange = layoutManager.glyphRange(for: textContainer)
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
Use CoreText, a more powerful and low-level API for laying out text. This would probably be unnecessarily complicated for this task.
Regardless of what you choose to use to measure text, you will probably need to do two passes: The first pass is to account for long words that should not be broken up over multiple lines, where you will need to find the largest font size that fits the largest (~longest) word fully within the label's bounds. In the second pass, you can continue your search downwards from the result of the first pass to find an even smaller font size as required to fit the entire text this time.
When doing the largest word measurement (rather than the entire text), you don't want to restrict the width parameter that you supply to some of the above sizing functions, otherwise the system will have no choice but to break up the single word you gave it and return incorrect results for your purposes. You will need to replace the width argument of the above methods with CGFloat.greatestFiniteMagnitude
:
- the width part of the size argument of
boundingRect()
.
- the width part of the size argument of
NSTextContainer()
.
You will also need to consider the actual algorithm that you use for your search, since measuring text is an expensive operation and a linear search might be too slow, depending on your needs. If you want to be more efficient you can apply a binary search instead. 🚀
For a robust working solution based on the above, see my open-source framework AccessibilityKit. A couple of examples of AKLabel
in action:
Hope this helps!