On an iPhone how do I calculate the size of a character in pixels for a given point size?
Point sizes are defined as 1/72 of an inch. That is, a 72-point font is approximately 1 inch from the lowest descent to the highest ascent. So the maximum height of a glyph in a 72pt font is about 1 inch.
Apple's iphone tech specs page claims that the iPhone currently has a resolution of 163 pixels per inch. So 72 points is 163 pixels, or about 2.2639 pixels per point. Just remember that every glyph varies in height and width, so this is a very rough estimate of size. Generally, the distance between baselines will be a bit larger than the font's point size so that lines of text don't crash into each other.
If you need exact measurements (and you probably do) then you'll need to actually measure the font glyphs using the font metric information. You can do this by using NSString's UIKit additions, which will let you measure the size of a particular string when rendered on screen.
To match font sizes (in Points) on the iPhone4 with font sizes (in Points) in Photoshop you have to set your Photoshop document to 144dpi. I have run a number of tests and that's the resolution that produces 1:1 results.
Steps:
Take a screenshot of “Settings » General » Accessibility » Large Text” on an iPhone4
Open the screenshot in Photoshop
Change the resolution from 72dpi to 144dpi with “Resample Image” off
Retype the text in Photoshop (in Points) to match size in the screenshot
I have gone through a number of different resolutions, including the 163dpi that was mentioned in the answer above, and I found that 144dpi produces 1:1 results. I have also tested this against a native app where I know the point sizes and the 144dpi was match there too.
Our graphic artist was very specific on certain devices to use pixel sizes instead of point size.
The function below will return a font based on pixel size.
It uses a brute force method to find the closet font, but then caches the results so next time the return will be very fast. I always appreciate comments on how this code could be made better. I use this function as a static class member in class called utils.
You can easily paste into any class you are using.
Hope it is of some help.
/** return a font as close to a pixel size as possible
example:
UIFont *font = [Utils fontWithName:#"HelveticaNeue-Medium" sizeInPixels:33];
#param fontName name of font same as UIFont fontWithName
#param sizeInPixels size in pixels for font
*/
+(UIFont *) fontWithName:(NSString *) fontName sizeInPixels:(CGFloat) pixels {
static NSMutableDictionary *fontDict; // to hold the font dictionary
if ( fontName == nil ) {
// we default to #"HelveticaNeue-Medium" for our default font
fontName = #"HelveticaNeue-Medium";
}
if ( fontDict == nil ) {
fontDict = [ #{} mutableCopy ];
}
// create a key string to see if font has already been created
//
NSString *strFontHash = [NSString stringWithFormat:#"%#-%f", fontName , pixels];
UIFont *fnt = fontDict[strFontHash];
if ( fnt != nil ) {
return fnt; // we have already created this font
}
// lets play around and create a font that falls near the point size needed
CGFloat pointStart = pixels/4;
CGFloat lastHeight = -1;
UIFont * lastFont = [UIFont fontWithName:fontName size:.5];\
NSMutableDictionary * dictAttrs = [ #{ } mutableCopy ];
NSString *fontCompareString = #"Mgj^";
for ( CGFloat pnt = pointStart ; pnt < 1000 ; pnt += .5 ) {
UIFont *font = [UIFont fontWithName:fontName size:pnt];
if ( font == nil ) {
NSLog(#"Unable to create font %#" , fontName );
NSAssert(font == nil, #"font name not found in fontWithName:sizeInPixels" ); // correct the font being past in
}
dictAttrs[NSFontAttributeName] = font;
CGSize cs = [fontCompareString sizeWithAttributes:dictAttrs];
CGFloat fheight = cs.height;
if ( fheight == pixels ) {
// that will be rare but we found it
fontDict[strFontHash] = font;
return font;
}
if ( fheight > pixels ) {
if ( lastFont == nil ) {
fontDict[strFontHash] = font;
return font;
}
// check which one is closer last height or this one
// and return the user
CGFloat fc1 = fabs( fheight - pixels );
CGFloat fc2 = fabs( lastHeight - pixels );
// return the smallest differential
if ( fc1 < fc2 ) {
fontDict[strFontHash] = font;
return font;
} else {
fontDict[strFontHash] = lastFont;
return lastFont;
}
}
lastFont = font;
lastHeight = fheight;
}
NSAssert( false, #"Hopefully should never get here");
return nil;
}
I believe you're looking for the UIFont NSString extensions that allow you to calculate the size of a string given a UIFont.
Here is the Link
Specifically the sizeWithFont methods.
You can't reliably convert points to pixels as the ppi (points-per-inch) will change from monitor to monitor. Have a read;
http://hsivonen.iki.fi/units/
Convert Pixels to Points
That said, some people have put together a few reference tables and calculators that may get you started;
http://www.unitconversion.org/typography/postscript-points-to-pixels-x-conversion.html
http://sureshjain.wordpress.com/2007/07/06/53/
The easiest way to get the pixel height for a given font and size is to use the boundingRect method on NSString. (I'm using #"Ap" here to make sure it contains a descender and an ascender.)
- (CGFloat)heightForFont:(UIFont *)font
{
NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
CGRect boundingRect = [#"Ap" boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:#{NSFontAttributeName:font} context:context];
return boundingRect.size.height;
}
I will add them as I figure them out:
A sizedToFit UILabel with a 12pt systemFont is 15px high.
You can calculate the pixel size on the fly by rendering a NSAttributedString and getting its size.
extension UIFont {
var pixelSize: CGFloat {
let string = "AWZgjpq"
let attributedString = NSMutableAttributedString(string: string)
attributedString.setAttributes([.font: self], range: NSRange(location: 0, length: string.count))
return attributedString.size().height
}
}
Further optimizations could be to add a lookup dictionary and cache results and/or make the test string not a variable (but the call itself is really fast). Also, if your font has some very irregular glyphs you can add them to the test string as well.
Usage: let pixelSize = label.font.pixelSize
Related
I have a button with changing text in it.
I want if the button text goes for third line it should reduce its font to minimumScaleFactor.
I am using this code
self.option1Button.titleLabel.minimumScaleFactor = .7;
self.option1Button.titleLabel.numberOfLines = 2;
self.option1Button.titleLabel.adjustsFontSizeToFitWidth = TRUE;
But this is not working. it doesn't change the font size when text reaches to third line.
The adjustToFit will only work for single line labels.
Try doing this:
//Create a string with the text we want to display.
self.ourText = #"This is your variable-length string. Assign it any way you want!";
/* This is where we define the ideal font that the Label wants to use.
Use the font you want to use and the largest font size you want to use. */
UIFont *font = [UIFont fontWithName:#"Marker Felt" size:28];
int i;
/* Time to calculate the needed font size.
This for loop starts at the largest font size, and decreases by two point sizes (i=i-2)
Until it either hits a size that will fit or hits the minimum size we want to allow (i > 10) */
for(i = 28; i > 10; i=i-2) {
// Set the new font size.
font = [font fontWithSize:i];
// You can log the size you're trying: NSLog(#"Trying size: %u", i);
/* This step is important: We make a constraint box
using only the fixed WIDTH of the UILabel. The height will
be checked later. */
CGSize constraintSize = CGSizeMake(260.0f, MAXFLOAT);
// This step checks how tall the label would be with the desired font.
CGSize labelSize = [self.ourText sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
/* Here is where you use the height requirement!
Set the value in the if statement to the height of your UILabel
If the label fits into your required height, it will break the loop
and use that font size. */
if(labelSize.height <= 180.0f)
break;
}
// You can see what size the function is using by outputting: NSLog(#"Best size is: %u", i);
// Set the UILabel's font to the newly adjusted font.
msg.font = font;
// Put the text into the UILabel outlet variable.
msg.text = self.ourText;
If you want your button to display three lines, you will have to set numberOfLines to 3. Strange but true.
I am implementing an IM app on iOS. I found that three20 library has a TTStyledTextLabel which provides cool features like showing images and url links. However I want to embed the TTStyledTextLabel in a message bubble (just like the sms app shipped with iphone does), where I need the label to adjust its size according to the text length. I found that TTStyledTextLabel can adjust its height according to its width, but I don't know how to make it shrink horizontally when the text is very short and can't fill up a whole line. Any suggestions?
I think I have a slightly better solution: I get the rootFrame of the ttstyledtext and iterate over its sibling frames to find the max width.
It works like this:
TTStyledTextLabel* label = [[TTStyledTextLabel alloc] init];
label.text = [TTStyledText textFromXHTML:myTextToBeDisplayed];
[label sizeToFit];
CGFloat maxWidth = 0;
TTStyledFrame *f = label.text.rootFrame;
while (f) {
int w = f.x + f.width;
if (w > maxWidth) {
maxWidth = w;
}
f = f.nextFrame;
}
return CGSizeMake(maxWidth, label.height);
I tried doing it by incrementally passing the width parameter in size to sizeToFit and looking at the resulting height to give cues in terms of whether the size is ok. But this is not a elegant solution
for (int index = 100; index < 320; index= index+30)
{
label.width = x;
if (label.height < 20)
break;
}
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.
Here is the most elegant (yet somewhat hackish) way I found to make this work:
Split the string into words
Calculate the width of each word using the current font size
Reduce the size of the string until each the word fits into one line
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;
}
}
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
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
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!
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()
}
A "quicky": how can I get the size (width) of a NSString?
I'm trying to see if the string width of a string to see if it is bigger than a given width of screen, case in which I have to "crop" it and append it with "...", getting the usual behavior of a UILabel. string.length won't do the trick since AAAAAAAA and iiiiii have the same length but different sizes (for example).
I'm kind of stuck.
Thanks a lot.
This is a different approach. Find out the minimum size of the text so that it won't wrap to more than one line. If it wraps to over one line, you can find out using the height.
You can use this code:
CGSize maximumSize = CGSizeMake(300, 9999);
NSString *myString = #"This is a long string which wraps";
UIFont *myFont = [UIFont fontWithName:#"Helvetica" size:14];
CGSize myStringSize = [myString sizeWithFont:myFont
constrainedToSize:maximumSize
lineBreakMode:self.myLabel.lineBreakMode];
300 is the width of the screen with a little space for margins. You should substitute your own values for font and size, and for the lineBreakMode if you're not using IB.
Now myStringSize will contain a height which you can check against the height of something you know is only 1 line high (using the same font and size). If it's bigger, you'll need to cut the text. Note that you should add a ... to the string before you check it again (adding the ... might push it over the limit again).
Put this code in a loop to cut the text, then check again for the correct height.
Use below method.
Objective-C
- (CGSize)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, frame.size.height + 1);
}
return size;
}
Swift 3.0
func findHeight(forText text: String, havingWidth widthValue: CGFloat, andFont font: UIFont) -> CGSize {
var size = CGSizeZero
if text {
var frame = text.boundingRect(withSize: CGSize(width: widthValue, height: CGFLOAT_MAX), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
size = CGSize(width: frame.size.width, height: frame.size.height + 1)
}
return size
}
You need to use Core Graphics to measure the string, as rendered in your specified font and size. See the answers to Measuring the pixel width of a string for a walkthrough.
sizeWithFont:constrainedToSize:lineBreakMode
is deprecated now. Use below code snippet,
UIFont *font=[UIFont fontWithName:#"Arial" size:16.f];
NSString *name = #"APPLE";
CGSize size = [name sizeWithAttributes:#{NSFontAttributeName:font}];
For whatever its worth --- I think the OP takes the wrong way to get there... if the measurement of width only serves to find the place where text should be clipped, and followed by ellipsis --- then OP should be aware of that this facility is implemented in all Text Views in Cocoa...
Pay attention to this enumeration:
typedef NS_ENUM(NSUInteger, NSLineBreakMode) {
NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default
NSLineBreakByCharWrapping, // Wrap at character boundaries
NSLineBreakByClipping, // Simply clip
NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz"
NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..."
NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz"
} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0));
By setting the line breaking mode of your text-field or text view to NSLineBreakByTruncatingTail, you'll achieve what you want, and probably at higher quality, without implementing yourself.
In this question I asked for a good way to truncate a string to fit a given UITextView. Since there was no way provided by the SDK directly, I've ended up writing the recursive method below (only called by the following public method). However, this doesn't work unless I subtract a fudge factor of 15 (kFudgeFactor) from the field width when calculating the string's height. If I don't do that, the string returned is actually too long for the field, and displays in an extra line below it. Anyone any idea why, and what I should really use instead of this fudge factor?
#pragma mark Size string to fit the new view
#define kFudgeFactor 15.0
#define kMaxFieldHeight 9999.0
// recursive method called by the main API
-(NSString*) sizeStringToFit:(NSString*)aString min:(int)aMin max:(int)aMax
{
if ((aMax-aMin) <= 1)
{
NSString* subString = [aString substringToIndex:aMin];
return subString;
}
int mean = (aMin + aMax)/2;
NSString* subString = [aString substringToIndex:mean];
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [subString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
if (stringSize.height <= self.frame.size.height)
return [self sizeStringToFit:aString min:mean max:aMax]; // too small
else
return [self sizeStringToFit:aString min:aMin max:mean];// too big
}
-(NSString*)sizeStringToFit:(NSString*)aString
{
CGSize tallerSize = CGSizeMake(self.frame.size.width-kFudgeFactor,kMaxFieldHeight);
CGSize stringSize = [aString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
// if it fits, just return
if (stringSize.height < self.frame.size.height)
return aString;
// too big - call the recursive method to size it
NSString* smallerString = [self sizeStringToFit:aString min:0 max:[aString length]];
return smallerString;
}
UIScrollView seems to use a fixed 8-pixel inset on both sides. This is independent of alignment or font size (based on testing & observation, not any explicit knowledge of the internals).
So it seems you are right to use your fudge factor, but it should probably be 16.0, not 15.0.
This is probably because the frame of the UIView is not the same size as the content view.
UITextView subclasses from UIScrollView.