Resizing text to fit available space

My app displays some text that should appear the same regardless of the container view or window size, i.e. it should grow and shrink with the container view or window.

On iOS there is UILabel.adjustsFontSizeToFitWidth but I couldn't find any equivalent API on macOS. On the internet some people suggest to iteratively set a smaller font size until the text fits the available space, but I thought there must be a more efficient solution. How does UILabel.adjustsFontSizeToFitWidth do it?

My expectation was that setting a font's size to a fraction of the window width or height would do the trick, but when resizing the window I can see a slightly different portion of it.

class ViewController: NSViewController {

    override func loadView() {
        view = MyView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
        NSLayoutConstraint.activate([view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 3), view.heightAnchor.constraint(greaterThanOrEqualToConstant: 100)])
    }

}

class MyView: NSView {
    let textField = NSTextField(labelWithString: String(repeating: "a b c d e f g h i j k l m n o p q r s t u v w x y z ", count: 2))
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        addSubview(textField)
        NSLayoutConstraint.activate([textField.topAnchor.constraint(equalTo: topAnchor), textField.leadingAnchor.constraint(equalTo: leadingAnchor), textField.trailingAnchor.constraint(equalTo: trailingAnchor)])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func resize(withOldSuperviewSize oldSize: NSSize) {
//        textField.font = .systemFont(ofSize: frame.width * 0.05)
        textField.font = .systemFont(ofSize: frame.height * 0.1)
    }
}
Answered by DTS Engineer in 859302022

By "some text that should appear the same," did you mean that the text size (both width and height) changes proportionally with the window size? If yes, changing the font size doesn't quite help.

The reason is that the text size doesn't change proportionally with the font size, and this is as-designed to achieve better text clarity. Concretely, if you double your window size (both width and height), your code will double the font size, and yet, the width of the spaces in your text (and also other characters) won't be doubled. As a result, your window will show more characters. For multi-line text, this in-proportional change can lead to different line break points, the text can hence look very different.

To achieve a feature similar to UILabel.adjustsFontSizeToFitWidth on macOS, I don't see a great solution, and hence would suggest that you file a feedback report to request an API for that purpose – If you do so, please share your report ID here for folks to track.

You might still "iteratively set a smaller font size until the text fits the available space," and try to improve the efficiency in some way. For brain storming, here is an example:

  1. Pick a character that has an average width. Assuming it is a piece of English text, "i" is probably the thinnest character and "W" or "M" are probably the widest ones.

  2. Use the character + size(withAttributes:) to calculate the average character width of the current font.

  3. Estimate the length of your text by timing the width with the character count of the text.

  4. Compare the estimation against your taget width (the window width), and use the difference to estimate a good font size.

  5. Use the font with the estimated font size + size(withAttributes:) to calculate the real width of your text.

  6. Repeat from step 2, until the real width is good enough.

I haven't really implemented this flow, but step 2 ~ 4 may reduce the calls of size(withAttributes:), which may help, especially when the text is relatively long.

Worth mentioning though, adjustsFontSizeToFitWidth doesn't provide a pixel perfection result. If your use case requires pixel perfection, consider converting your text to an image and scale the image.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Could the problem be in the approximation when using fractional font size ?

Did you try using

func size(withAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> CGSize

and adapt the size to fit window width ?

Some more details here: https://stackoverflow.com/questions/1324379/how-to-calculate-the-width-of-a-text-string-of-a-specific-font-and-font-size

That's exactly the question: how do I adapt the size to fit the window width? I don't see how size(withAttributes:) would help me finding out the correct font size with a single call.

I don't know if that answers, but I would:

  • considering present font size is actualSize
  • and windowWidth are pastWidth and newWidth
  • let newSize = text.size(withAttributes: actualSize) * newWidth / pastWidth
  • change font size to newSize

size(withAttributes:) returns CGSize not a number. Why wouldn't you calculate newSize = actualSize * newWidth / pastWidth instead? But that's the same as calculating newSize = newWidth * constant, which as I showed doesn't work.

You're right.

I need to go and test in code.

I'll check what is the new text width with the new size and see if you need an extra adjustment (because string with may not be exactly proportional to font width).

Did you try with monospaced font, just to see.

If it works, that confirms that the cause is that string width is not exactly proportional to font size.

With the system monospaced font it seems to work, regardless whether I use the window width or height to calculate the font size. On the other hand, with the standard system font, it doesn't work with either width or height. I thought the font size is related to the height, in which case using a monospaced font (where characters have equal width) should make no difference.

By "some text that should appear the same," did you mean that the text size (both width and height) changes proportionally with the window size? If yes, changing the font size doesn't quite help.

The reason is that the text size doesn't change proportionally with the font size, and this is as-designed to achieve better text clarity. Concretely, if you double your window size (both width and height), your code will double the font size, and yet, the width of the spaces in your text (and also other characters) won't be doubled. As a result, your window will show more characters. For multi-line text, this in-proportional change can lead to different line break points, the text can hence look very different.

To achieve a feature similar to UILabel.adjustsFontSizeToFitWidth on macOS, I don't see a great solution, and hence would suggest that you file a feedback report to request an API for that purpose – If you do so, please share your report ID here for folks to track.

You might still "iteratively set a smaller font size until the text fits the available space," and try to improve the efficiency in some way. For brain storming, here is an example:

  1. Pick a character that has an average width. Assuming it is a piece of English text, "i" is probably the thinnest character and "W" or "M" are probably the widest ones.

  2. Use the character + size(withAttributes:) to calculate the average character width of the current font.

  3. Estimate the length of your text by timing the width with the character count of the text.

  4. Compare the estimation against your taget width (the window width), and use the difference to estimate a good font size.

  5. Use the font with the estimated font size + size(withAttributes:) to calculate the real width of your text.

  6. Repeat from step 2, until the real width is good enough.

I haven't really implemented this flow, but step 2 ~ 4 may reduce the calls of size(withAttributes:), which may help, especially when the text is relatively long.

Worth mentioning though, adjustsFontSizeToFitWidth doesn't provide a pixel perfection result. If your use case requires pixel perfection, consider converting your text to an image and scale the image.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

By "some text that should appear the same," did you mean that the text size (both width and height) changes proportionally with the window size?

Correct.

Worth mentioning though, adjustsFontSizeToFitWidth doesn't provide a pixel perfection result.

I haven't used UILabel.adjustsFontSizeToFitWidth before, so I'm not sure how it actually behaves, and I'm also not sure what you mean with "pixel perfection".

I'm looking for a solution that resizes the text so that it stays proportional to the container view frame (which has a fixed aspect ratio). To be specific, I'm simulating subtitles on top of a video.

While playing with NSString.size(withAttributes:) and NSAttributedString.size(), I noticed that these methods only allow me to determine the size of a string when laid out on a single, infinite line. There is no equivalent to NSString.draw(in:withAttributes:) that also takes a rectangle. Should I instead use NSTextView (in which the text is eventually laid out anyway) and query its frame property?

I just tried to load a video with subtitles in Safari, and the subtitles resize perfectly with the video (or at least the subtitle width is always proportional to the video width, while the height seems to be adjusted so that the line height is an integer, i.e. it changes in small jumps). Is it a secret how it's done, or can you share how that's implemented?

Even a text paragraph in a website, when applied the CSS font-size: 20vh, resizes perfectly by keeping a constant ratio between container height and font height. Is there a way to adopt this behaviour in AppKit or should I just use a WKWebView instead of a NSTextView? Although from my first tests it looks like a web view always sets its own background, so it's not ideal to use as a text overlay.

Resizing text to fit available space
 
 
Q