Reputation: 6259
This is part of an iPhone application but should apply to Cocoa written in objC in general.
I have a UILabel holding various amounts of text (from single characters to several sentences). The text should always be displayed in the largest possible font that fits all the text within the UILabel. The maximum number of lines is set to 4 and the line break mode is set to word wrap.
Since multiple lines are used, adjustsFontSizeToFitWidth won't work for resizing the text.
Thus I am using a loop to determine the largest possible font size for each string as such:
//Set the text self.textLabel.text = text; //Largest size used NSInteger fsize = 200; textLabel.font = [UIFont fontWithName:@"Verdana-Bold" size:fsize]; //Calculate size of the rendered string with the current parameters float height = [text sizeWithFont:textLabel.font constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999) lineBreakMode:UILineBreakModeWordWrap].height; //Reduce font size by 5 while too large, break if no height (empty string) while (height > textLabel.bounds.size.height and height != 0) { fsize -= 5; textLabel.font = [UIFont fontWithName:@"Verdana-Bold" size:fsize]; height = [text sizeWithFont:textLabel.font constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999) lineBreakMode:UILineBreakModeWordWrap].height; };
This approach works well for the most part. The exception are long words. Let's take the string @"The experience foo." as an example. The word "experience", being much longer than the others will be split in half without being word-wrapped correctly and the string split across 4 lines. I am looking for a way to reduce the size further so that each individual word fits in one line.
Example:
-old-
Font size: 60
The Exper ience foo
should be
-new-
Font size: 30
The Experience foo
There probably is an easy way to do this but I'm hitting a wall.
Upvotes: 9
Views: 9808
Reputation: 741
UILabel extension in Swift 4 based on 0x90's answer:
func adjustFontSizeToFit() {
guard var font = self.font, let text = self.text else { return }
let size = self.frame.size
var maxSize = font.pointSize
while maxSize >= self.minimumScaleFactor * self.font.pointSize {
font = font.withSize(maxSize)
let constraintSize = CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)
let textRect = (text as NSString).boundingRect(with: constraintSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font : font], context: nil)
let labelSize = textRect.size
if labelSize.height <= size.height {
self.font = font
self.setNeedsLayout()
break
}
maxSize -= 1
}
self.font = font;
self.setNeedsLayout()
}
Upvotes: 0
Reputation: 41
It's a great question, and you would think that using the largest possible font size without breaking words up would be part of the built-in UIKit
functionality or a related framework by now. Here's a good visual example of the question:
As described by others, the trick is to perform the size search for individual words, as well as the entire text as a whole. This is because when you specify a width to draw single words into, the sizing methods will break the words up since they have no other choice - you are asking them to draw an "unbreakable" string, with a specific font size, into a region that simply doesn't fit.
At the heart of my working solution, I use the following binary search function:
func binarySearch(string: NSAttributedString, minFontSize: CGFloat, maxFontSize: CGFloat, maxSize: CGSize, options: NSStringDrawingOptions) -> CGFloat {
let avgSize = roundedFontSize((minFontSize + maxFontSize) / 2)
if avgSize == minFontSize || avgSize == maxFontSize { return minFontSize }
let singleLine = !options.contains(.usesLineFragmentOrigin)
let canvasSize = CGSize(width: singleLine ? .greatestFiniteMagnitude : maxSize.width, height: .greatestFiniteMagnitude)
if maxSize.contains(string.withFontSize(avgSize).boundingRect(with: canvasSize, options: options, context: nil).size) {
return binarySearch(string: string, minFontSize:avgSize, maxFontSize:maxFontSize, maxSize: maxSize, options: options)
} else {
return binarySearch(string: string, minFontSize:minFontSize, maxFontSize:avgSize, maxSize: maxSize, options: options)
}
}
This alone is not enough though. You need to use it to first find the maximum size that will fit the longest word inside the bounds. Once you have that, continue searching for a smaller size until the entire text fits. This way no word is ever going to be broken up. There are some additional considerations that are a bit more involved, including finding what the longest word actually is (there's some gotchas!) and iOS font caching performance.
If you only care about showing the text on the screen in an easy way, I have developed a robust implementation in Swift, which I'm also using in a production app. It's a UIView
subclass with efficient, automatic font scaling for any input text, including multiple lines. To use it, you'd simply do something like:
let view = AKTextView()
// Use a simple or fancy NSAttributedString
view.attributedText = .init(string: "Some text here")
// Add to the view hierarchy somewhere
That's it! You can find the complete source code here: https://github.com/FlickType/AccessibilityKit
Hope this helps!
Upvotes: 1
Reputation: 6259
Here is the most elegant (yet somewhat hackish) way I found to make this work:
Resource consumption is low enough for this to work even in UITableViews
full of strings edited this way.
Here is the new code:
//Set the text
self.textLabel.text = text;
//Largest size used
NSInteger fsize = 200; textLabel.font = [UIFont fontWithName:@"Verdana-Bold"
size:fsize];
//Calculate size of the rendered string with the current parameters
float height =
[text sizeWithFont:textLabel.font
constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999)
lineBreakMode:UILineBreakModeWordWrap].height;
//Reduce font size by 5 while too large, break if no height (empty string)
while (height > textLabel.bounds.size.height and height != 0) {
fsize -= 5;
textLabel.font = [UIFont fontWithName:@"Verdana-Bold" size:fsize];
height = [text sizeWithFont:textLabel.font
constrainedToSize:CGSizeMake(textLabel.bounds.size.width,99999)
lineBreakMode:UILineBreakModeWordWrap].height;
};
// Loop through words in string and resize to fit
for (NSString *word in [text componentsSeparatedByString:@" "]) {
float width = [word sizeWithFont:textLabel.font].width;
while (width > textLabel.bounds.size.width and width != 0) {
fsize -= 3;
textLabel.font = [UIFont fontWithName:@"Verdana-Bold" size:fsize];
width = [word sizeWithFont:textLabel.font].width;
}
}
Upvotes: 13
Reputation: 3659
Here's my version of 0x90's answer in a category:
@implementation UILabel (MultilineAutosize)
- (void)adjustFontSizeToFit
{
//Largest size used
NSInteger fsize = self.font.pointSize;
//Calculate size of the rendered string with the current parameters
float height = [self.text sizeWithFont:self.font
constrainedToSize:CGSizeMake(self.bounds.size.width, MAXFLOAT)
lineBreakMode:NSLineBreakByWordWrapping].height;
//Reduce font size by 5 while too large, break if no height (empty string)
while (height > self.bounds.size.height && height > 0) {
fsize -= 5;
self.font = [self.font fontWithSize:fsize];
height = [self.text sizeWithFont:self.font
constrainedToSize:CGSizeMake(self.bounds.size.width, MAXFLOAT)
lineBreakMode:NSLineBreakByWordWrapping].height;
};
// Loop through words in string and resize to fit
for (NSString *word in [self.text componentsSeparatedByString:@" "]) {
float width = [word sizeWithFont:self.font].width;
while (width > self.bounds.size.width && width > 0) {
fsize -= 3;
self.font = [self.font fontWithSize:fsize];
width = [word sizeWithFont:self.font].width;
}
}
}
@end
Upvotes: 4
Reputation: 31
You can use the code above in a Category for UILabel
UILabel+AdjustFontSize.h
@interface UILabel (UILabel_AdjustFontSize)
- (void) adjustsFontSizeToFitWidthWithMultipleLinesFromFontWithName:(NSString*)fontName size:(NSInteger)fsize andDescreasingFontBy:(NSInteger)dSize;
@end
UILabel+AdjustFontSize.m
@implementation UILabel (UILabel_AdjustFontSize)
- (void) adjustsFontSizeToFitWidthWithMultipleLinesFromFontWithName:(NSString*)fontName size:(NSInteger)fsize andDescreasingFontBy:(NSInteger)dSize{
//Largest size used
self.font = [UIFont fontWithName:fontName size:fsize];
//Calculate size of the rendered string with the current parameters
float height = [self.text sizeWithFont:self.font
constrainedToSize:CGSizeMake(self.bounds.size.width,99999)
lineBreakMode:UILineBreakModeWordWrap].height;
//Reduce font size by dSize while too large, break if no height (empty string)
while (height > self.bounds.size.height && height != 0) {
fsize -= dSize;
self.font = [UIFont fontWithName:fontName size:fsize];
height = [self.text sizeWithFont:self.font
constrainedToSize:CGSizeMake(self.bounds.size.width,99999)
lineBreakMode:UILineBreakModeWordWrap].height;
};
// Loop through words in string and resize to fit
for (NSString *word in [self.text componentsSeparatedByString:@" "]) {
float width = [word sizeWithFont:self.font].width;
while (width > self.bounds.size.width && width != 0) {
fsize -= dSize;
self.font = [UIFont fontWithName:fontName size:fsize];
width = [word sizeWithFont:self.font].width;
}
}
}
@end
Upvotes: 3